@rpgjs/client 5.0.0-alpha.22 → 5.0.0-alpha.24

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 (134) hide show
  1. package/dist/Game/Object.d.ts +2 -0
  2. package/dist/RpgClientEngine.d.ts +115 -9
  3. package/dist/components/gui/mobile/index.d.ts +8 -0
  4. package/dist/components/prebuilt/index.d.ts +1 -0
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.js +13 -8
  7. package/dist/index.js.map +1 -1
  8. package/dist/index10.js +1 -1
  9. package/dist/index11.js +6 -5
  10. package/dist/index11.js.map +1 -1
  11. package/dist/index12.js +2 -2
  12. package/dist/index13.js +102 -10
  13. package/dist/index13.js.map +1 -1
  14. package/dist/index14.js +67 -9
  15. package/dist/index14.js.map +1 -1
  16. package/dist/index15.js +10 -263
  17. package/dist/index15.js.map +1 -1
  18. package/dist/index16.js +9 -97
  19. package/dist/index16.js.map +1 -1
  20. package/dist/index17.js +300 -89
  21. package/dist/index17.js.map +1 -1
  22. package/dist/index18.js +63 -80
  23. package/dist/index18.js.map +1 -1
  24. package/dist/index19.js +96 -348
  25. package/dist/index19.js.map +1 -1
  26. package/dist/index2.js +176 -24
  27. package/dist/index2.js.map +1 -1
  28. package/dist/index20.js +360 -17
  29. package/dist/index20.js.map +1 -1
  30. package/dist/index21.js +19 -50
  31. package/dist/index21.js.map +1 -1
  32. package/dist/index22.js +212 -5
  33. package/dist/index22.js.map +1 -1
  34. package/dist/index23.js +6 -395
  35. package/dist/index23.js.map +1 -1
  36. package/dist/index24.js +4 -39
  37. package/dist/index24.js.map +1 -1
  38. package/dist/index25.js +19 -20
  39. package/dist/index25.js.map +1 -1
  40. package/dist/index26.js +43 -2624
  41. package/dist/index26.js.map +1 -1
  42. package/dist/index27.js +5 -110
  43. package/dist/index27.js.map +1 -1
  44. package/dist/index28.js +394 -65
  45. package/dist/index28.js.map +1 -1
  46. package/dist/index29.js +40 -15
  47. package/dist/index29.js.map +1 -1
  48. package/dist/index3.js +3 -3
  49. package/dist/index30.js +21 -23
  50. package/dist/index30.js.map +1 -1
  51. package/dist/index31.js +2624 -86
  52. package/dist/index31.js.map +1 -1
  53. package/dist/index32.js +107 -34
  54. package/dist/index32.js.map +1 -1
  55. package/dist/index33.js +69 -22
  56. package/dist/index33.js.map +1 -1
  57. package/dist/index34.js +19 -3
  58. package/dist/index34.js.map +1 -1
  59. package/dist/index35.js +21 -329
  60. package/dist/index35.js.map +1 -1
  61. package/dist/index36.js +91 -30
  62. package/dist/index36.js.map +1 -1
  63. package/dist/index37.js +37 -7
  64. package/dist/index37.js.map +1 -1
  65. package/dist/index38.js +22 -9
  66. package/dist/index38.js.map +1 -1
  67. package/dist/index39.js +139 -10
  68. package/dist/index39.js.map +1 -1
  69. package/dist/index4.js +3 -3
  70. package/dist/index40.js +16 -6
  71. package/dist/index40.js.map +1 -1
  72. package/dist/index41.js +1 -325
  73. package/dist/index41.js.map +1 -1
  74. package/dist/index42.js +530 -3680
  75. package/dist/index42.js.map +1 -1
  76. package/dist/index43.js +24 -67
  77. package/dist/index43.js.map +1 -1
  78. package/dist/index44.js +9 -184
  79. package/dist/index44.js.map +1 -1
  80. package/dist/index45.js +6 -503
  81. package/dist/index45.js.map +1 -1
  82. package/dist/index46.js +325 -2
  83. package/dist/index46.js.map +1 -1
  84. package/dist/index47.js +3687 -17
  85. package/dist/index47.js.map +1 -1
  86. package/dist/index48.js +69 -202
  87. package/dist/index48.js.map +1 -1
  88. package/dist/index49.js +182 -7
  89. package/dist/index49.js.map +1 -1
  90. package/dist/index5.js +1 -1
  91. package/dist/index50.js +497 -106
  92. package/dist/index50.js.map +1 -1
  93. package/dist/index51.js +48 -130
  94. package/dist/index51.js.map +1 -1
  95. package/dist/index52.js +17 -134
  96. package/dist/index52.js.map +1 -1
  97. package/dist/index53.js +3 -109
  98. package/dist/index53.js.map +1 -1
  99. package/dist/index54.js +9 -138
  100. package/dist/index54.js.map +1 -1
  101. package/dist/index55.js +111 -7
  102. package/dist/index55.js.map +1 -1
  103. package/dist/index56.js +130 -48
  104. package/dist/index56.js.map +1 -1
  105. package/dist/index57.js +137 -0
  106. package/dist/index57.js.map +1 -0
  107. package/dist/index58.js +112 -0
  108. package/dist/index58.js.map +1 -0
  109. package/dist/index59.js +9 -0
  110. package/dist/index59.js.map +1 -0
  111. package/dist/index6.js +1 -1
  112. package/dist/index7.js +1 -1
  113. package/dist/index8.js +17 -2
  114. package/dist/index8.js.map +1 -1
  115. package/dist/index9.js +10 -27
  116. package/dist/index9.js.map +1 -1
  117. package/dist/services/keyboardControls.d.ts +1 -2
  118. package/dist/services/mmorpg.d.ts +1 -1
  119. package/dist/services/standalone.d.ts +1 -1
  120. package/package.json +9 -9
  121. package/src/Game/Object.ts +8 -0
  122. package/src/Gui/Gui.ts +4 -31
  123. package/src/RpgClientEngine.ts +193 -20
  124. package/src/components/character.ce +146 -9
  125. package/src/components/gui/mobile/index.ts +24 -0
  126. package/src/components/gui/mobile/mobile.ce +80 -0
  127. package/src/components/prebuilt/index.ts +1 -0
  128. package/src/components/prebuilt/light-halo.ce +148 -0
  129. package/src/components/scenes/canvas.ce +2 -2
  130. package/src/components/scenes/event-layer.ce +1 -0
  131. package/src/components/scenes/transition.ce +60 -0
  132. package/src/index.ts +6 -1
  133. package/src/module.ts +15 -0
  134. package/src/services/keyboardControls.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/client",
3
- "version": "5.0.0-alpha.22",
3
+ "version": "5.0.0-alpha.24",
4
4
  "description": "RPGJS is a framework for creating RPG/MMORPG games",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,18 +22,18 @@
22
22
  "pixi.js": "^8.9.2"
23
23
  },
24
24
  "dependencies": {
25
- "@rpgjs/client": "5.0.0-alpha.22",
26
- "@rpgjs/common": "5.0.0-alpha.22",
27
- "@rpgjs/server": "5.0.0-alpha.22",
28
- "@signe/di": "^2.5.2",
29
- "@signe/room": "^2.5.2",
30
- "@signe/sync": "^2.5.2",
25
+ "@rpgjs/client": "5.0.0-alpha.24",
26
+ "@rpgjs/common": "5.0.0-alpha.24",
27
+ "@rpgjs/server": "5.0.0-alpha.24",
28
+ "@signe/di": "^2.6.0",
29
+ "@signe/room": "^2.6.0",
30
+ "@signe/sync": "^2.6.0",
31
31
  "pixi-filters": "^6.1.5",
32
32
  "rxjs": "^7.8.2"
33
33
  },
34
34
  "devDependencies": {
35
- "@canvasengine/compiler": "2.0.0-beta.37",
36
- "vite": "^7.2.6",
35
+ "@canvasengine/compiler": "2.0.0-beta.40",
36
+ "vite": "^7.2.7",
37
37
  "vite-plugin-dts": "^4.5.4",
38
38
  "vitest": "^4.0.15"
39
39
  },
@@ -241,4 +241,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
241
241
  const engine = inject(RpgClientEngine);
242
242
  engine.getComponentAnimation(id).displayEffect(params, this);
243
243
  }
244
+
245
+ isEvent(): boolean {
246
+ return this.type === 'event';
247
+ }
248
+
249
+ isPlayer(): boolean {
250
+ return this.type === 'player';
251
+ }
244
252
  }
package/src/Gui/Gui.ts CHANGED
@@ -195,14 +195,13 @@ export class RpgGui {
195
195
  if (!guiId) {
196
196
  throw new Error("GUI must have a name or id");
197
197
  }
198
-
199
198
  const guiInstance: GuiInstance = {
200
199
  name: guiId,
201
200
  component: gui.component,
202
201
  display: signal(gui.display || false),
203
202
  data: signal(gui.data || {}),
204
203
  autoDisplay: gui.autoDisplay || false,
205
- dependencies: gui.dependencies,
204
+ dependencies: gui.dependencies ? gui.dependencies() : [],
206
205
  attachToSprite: gui.attachToSprite || false,
207
206
  };
208
207
 
@@ -317,8 +316,8 @@ export class RpgGui {
317
316
  // Handle Vue component display
318
317
  this._handleVueComponentDisplay(id, data, dependencies, guiInstance);
319
318
  } else {
320
- // Handle CanvasEngine component display
321
- this._handleCanvasComponentDisplay(id, data, dependencies, guiInstance);
319
+ guiInstance.data.set(data);
320
+ guiInstance.display.set(true);
322
321
  }
323
322
  }
324
323
 
@@ -371,33 +370,7 @@ export class RpgGui {
371
370
  * @param guiInstance - GUI instance
372
371
  */
373
372
  private _handleCanvasComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {
374
- // Unsubscribe from previous subscription if exists
375
- if (guiInstance.subscription) {
376
- guiInstance.subscription.unsubscribe();
377
- guiInstance.subscription = undefined;
378
- }
379
-
380
- // Use runtime dependencies or config dependencies
381
- const deps = dependencies.length > 0
382
- ? dependencies
383
- : (guiInstance.dependencies ? guiInstance.dependencies() : []);
384
-
385
- if (deps.length > 0) {
386
- // Subscribe to dependencies
387
- guiInstance.subscription = combineLatest(
388
- deps.map(dependency => dependency.observable)
389
- ).subscribe((values) => {
390
- if (values.every(value => value !== undefined)) {
391
- guiInstance.data.set(data);
392
- guiInstance.display.set(true);
393
- }
394
- });
395
- return;
396
- }
397
-
398
- // No dependencies, display immediately
399
- guiInstance.data.set(data);
400
- guiInstance.display.set(true);
373
+
401
374
  }
402
375
 
403
376
  /**
@@ -1,5 +1,5 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
- import { Context, inject } from "@signe/di";
2
+ import { inject } from './core/inject'
3
3
  import { signal, bootstrapCanvas, KeyboardControls, Howl, trigger } from "canvasengine";
4
4
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
5
5
  import { LoadMapService, LoadMapToken } from "./services/loadMap";
@@ -12,7 +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 { lastValueFrom, Observable } from "rxjs";
15
+ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from "rxjs";
16
16
  import { GlobalConfigToken } from "./module";
17
17
  import * as PIXI from "pixi.js";
18
18
  import { PrebuiltComponentAnimations } from "./components/animations";
@@ -53,6 +53,8 @@ export class RpgClientEngine<T = any> {
53
53
  /** Trigger for map shake animation */
54
54
  mapShakeTrigger = trigger();
55
55
 
56
+ controlsReady = signal(undefined);
57
+
56
58
  private predictionEnabled = false;
57
59
  private prediction?: PredictionController<Direction>;
58
60
  private readonly SERVER_CORRECTION_THRESHOLD = 30;
@@ -63,13 +65,19 @@ export class RpgClientEngine<T = any> {
63
65
  private pingInterval: any = null;
64
66
  private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
65
67
  private lastInputTime = 0;
66
-
67
- constructor(public context: Context) {
68
- this.webSocket = inject(context, WebSocketToken);
69
- this.guiService = inject(context, RpgGui);
70
- this.loadMapService = inject(context, LoadMapToken);
71
- this.hooks = inject<Hooks>(context, ModulesToken);
72
- this.globalConfig = inject(context, GlobalConfigToken)
68
+ // Track map loading state for onAfterLoading hook using RxJS
69
+ private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);
70
+ private playerIdReceived$ = new BehaviorSubject<boolean>(false);
71
+ private playersReceived$ = new BehaviorSubject<boolean>(false);
72
+ private eventsReceived$ = new BehaviorSubject<boolean>(false);
73
+ private onAfterLoadingSubscription?: any;
74
+
75
+ constructor(public context) {
76
+ this.webSocket = inject(WebSocketToken);
77
+ this.guiService = inject(RpgGui);
78
+ this.loadMapService = inject(LoadMapToken);
79
+ this.hooks = inject<Hooks>(ModulesToken);
80
+ this.globalConfig = inject(GlobalConfigToken)
73
81
 
74
82
  if (!this.globalConfig) {
75
83
  this.globalConfig = {} as T
@@ -127,11 +135,12 @@ export class RpgClientEngine<T = any> {
127
135
  * ```
128
136
  */
129
137
  setKeyboardControls(controlInstance: any) {
130
- const currentValues = this.context.values['inject:' + KeyboardControls]
131
- this.context.values['inject:' + KeyboardControls] = {
138
+ const currentValues = this.context.values['inject:' + 'KeyboardControls']
139
+ this.context.values['inject:' + 'KeyboardControls'] = {
132
140
  ...currentValues,
133
141
  values: new Map([['__default__', controlInstance]])
134
142
  }
143
+ this.controlsReady.set(true);
135
144
  }
136
145
 
137
146
  async start() {
@@ -189,12 +198,27 @@ export class RpgClientEngine<T = any> {
189
198
 
190
199
  private initListeners() {
191
200
  this.webSocket.on("sync", (data) => {
192
- if (data.pId) this.playerIdSignal.set(data.pId)
201
+ if (data.pId) {
202
+ this.playerIdSignal.set(data.pId);
203
+ // Signal that player ID was received
204
+ this.playerIdReceived$.next(true);
205
+ }
193
206
 
194
207
  // Apply client-side prediction filtering and server reconciliation
195
208
  this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
196
209
 
197
210
  load(this.sceneMap, data, true);
211
+
212
+ // Check if players and events are present in sync data
213
+ const players = data.players || this.sceneMap.players();
214
+ if (players && Object.keys(players).length > 0) {
215
+ this.playersReceived$.next(true);
216
+ }
217
+
218
+ const events = data.events || this.sceneMap.events();
219
+ if (events !== undefined) {
220
+ this.eventsReceived$.next(true);
221
+ }
198
222
  });
199
223
 
200
224
  // Handle pong responses for RTT measurement
@@ -266,7 +290,7 @@ export class RpgClientEngine<T = any> {
266
290
 
267
291
  this.webSocket.on("shakeMap", (data) => {
268
292
  const { intensity, duration, frequency, direction } = data || {};
269
- this.mapShakeTrigger.start({
293
+ (this.mapShakeTrigger as any).start({
270
294
  intensity,
271
295
  duration,
272
296
  frequency,
@@ -363,11 +387,25 @@ export class RpgClientEngine<T = any> {
363
387
  }
364
388
 
365
389
  private async loadScene(mapId: string) {
366
- this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap).subscribe();
390
+ await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
367
391
 
368
392
  // Clear client prediction states when changing maps
369
393
  this.clearClientPredictionStates();
370
394
 
395
+ // Reset all conditions for new map loading
396
+ this.mapLoadCompleted$.next(false);
397
+ this.playerIdReceived$.next(false);
398
+ this.playersReceived$.next(false);
399
+ this.eventsReceived$.next(false);
400
+
401
+ // Unsubscribe previous subscription if exists
402
+ if (this.onAfterLoadingSubscription) {
403
+ this.onAfterLoadingSubscription.unsubscribe();
404
+ }
405
+
406
+ // Setup RxJS observable to wait for all conditions
407
+ this.setupOnAfterLoadingObserver();
408
+
371
409
  this.webSocket.updateProperties({ room: mapId })
372
410
  await this.webSocket.reconnect(() => {
373
411
  this.initListeners()
@@ -375,7 +413,25 @@ export class RpgClientEngine<T = any> {
375
413
  })
376
414
  const res = await this.loadMapService.load(mapId)
377
415
  this.sceneMap.data.set(res)
378
- this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap).subscribe();
416
+
417
+ // Check if playerId is already present
418
+ if (this.playerIdSignal()) {
419
+ this.playerIdReceived$.next(true);
420
+ }
421
+
422
+ // Check if players and events are already present in sceneMap
423
+ const players = this.sceneMap.players();
424
+ if (players && Object.keys(players).length > 0) {
425
+ this.playersReceived$.next(true);
426
+ }
427
+
428
+ const events = this.sceneMap.events();
429
+ if (events !== undefined) {
430
+ this.eventsReceived$.next(true);
431
+ }
432
+
433
+ // Signal that map loading is completed (this should be last to ensure other checks are done)
434
+ this.mapLoadCompleted$.next(true);
379
435
  this.sceneMap.loadPhysic()
380
436
  }
381
437
 
@@ -787,13 +843,38 @@ export class RpgClientEngine<T = any> {
787
843
  * Add a component to render behind sprites
788
844
  * Components added with this method will be displayed with a lower z-index than the sprite
789
845
  *
790
- * @param component - The component to add behind sprites
791
- * @returns The added component
846
+ * Supports multiple formats:
847
+ * 1. Direct component: `ShadowComponent`
848
+ * 2. Configuration object: `{ component: LightHalo, props: {...} }`
849
+ * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
850
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
851
+ *
852
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
853
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
854
+ *
855
+ * @param component - The component to add behind sprites, or a configuration object
856
+ * @param component.component - The component function to render
857
+ * @param component.props - Static props object or function that receives the sprite object and returns props
858
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
859
+ * @returns The added component or configuration
792
860
  *
793
861
  * @example
794
862
  * ```ts
795
863
  * // Add a shadow component behind all sprites
796
864
  * engine.addSpriteComponentBehind(ShadowComponent);
865
+ *
866
+ * // Add a component with static props
867
+ * engine.addSpriteComponentBehind({
868
+ * component: LightHalo,
869
+ * props: { radius: 30 }
870
+ * });
871
+ *
872
+ * // Add a component with dynamic props and dependencies
873
+ * engine.addSpriteComponentBehind({
874
+ * component: HealthBar,
875
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
876
+ * dependencies: (object) => [object.hp, object.param.maxHp]
877
+ * });
797
878
  * ```
798
879
  */
799
880
  addSpriteComponentBehind(component: any) {
@@ -805,16 +886,41 @@ export class RpgClientEngine<T = any> {
805
886
  * Add a component to render in front of sprites
806
887
  * Components added with this method will be displayed with a higher z-index than the sprite
807
888
  *
808
- * @param component - The component to add in front of sprites
809
- * @returns The added component
889
+ * Supports multiple formats:
890
+ * 1. Direct component: `HealthBarComponent`
891
+ * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`
892
+ * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`
893
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
894
+ *
895
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
896
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
897
+ *
898
+ * @param component - The component to add in front of sprites, or a configuration object
899
+ * @param component.component - The component function to render
900
+ * @param component.props - Static props object or function that receives the sprite object and returns props
901
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
902
+ * @returns The added component or configuration
810
903
  *
811
904
  * @example
812
905
  * ```ts
813
906
  * // Add a health bar component in front of all sprites
814
907
  * engine.addSpriteComponentInFront(HealthBarComponent);
908
+ *
909
+ * // Add a component with static props
910
+ * engine.addSpriteComponentInFront({
911
+ * component: StatusIndicator,
912
+ * props: { type: 'poison' }
913
+ * });
914
+ *
915
+ * // Add a component with dynamic props and dependencies
916
+ * engine.addSpriteComponentInFront({
917
+ * component: HealthBar,
918
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
919
+ * dependencies: (object) => [object.hp, object.param.maxHp]
920
+ * });
815
921
  * ```
816
922
  */
817
- addSpriteComponentInFront(component: any) {
923
+ addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {
818
924
  this.spriteComponentsInFront.update((components: any[]) => [...components, component])
819
925
  return component
820
926
  }
@@ -884,6 +990,33 @@ export class RpgClientEngine<T = any> {
884
990
  return componentAnimation.instance
885
991
  }
886
992
 
993
+ /**
994
+ * Start a transition
995
+ *
996
+ * Convenience method to display a transition by its ID using the GUI system.
997
+ *
998
+ * @param id - The unique identifier of the transition to start
999
+ * @param props - Props to pass to the transition component
1000
+ *
1001
+ * @example
1002
+ * ```ts
1003
+ * // Start a fade transition
1004
+ * engine.startTransition('fade', { duration: 1000, color: 'black' });
1005
+ *
1006
+ * // Start with onFinish callback
1007
+ * engine.startTransition('fade', {
1008
+ * duration: 1000,
1009
+ * onFinish: () => console.log('Fade complete')
1010
+ * });
1011
+ * ```
1012
+ */
1013
+ startTransition(id: string, props: any = {}) {
1014
+ if (!this.guiService.exists(id)) {
1015
+ throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
1016
+ }
1017
+ this.guiService.display(id, props);
1018
+ }
1019
+
887
1020
  async processInput({ input }: { input: Direction }) {
888
1021
  const timestamp = Date.now();
889
1022
  let frame: number;
@@ -987,6 +1120,46 @@ export class RpgClientEngine<T = any> {
987
1120
  return this.sceneMap.getCurrentPlayer()
988
1121
  }
989
1122
 
1123
+ /**
1124
+ * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook
1125
+ *
1126
+ * This method uses RxJS `combineLatest` to wait for all conditions to be met,
1127
+ * regardless of the order in which they arrive:
1128
+ * 1. The map loading is completed (loadMapService.load is finished)
1129
+ * 2. We received a player ID (pId)
1130
+ * 3. Players array has at least one element
1131
+ * 4. Events property is present in the sync data
1132
+ *
1133
+ * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.
1134
+ *
1135
+ * ## Design
1136
+ *
1137
+ * Uses BehaviorSubjects to track each condition state, allowing events to arrive
1138
+ * in any order. The `combineLatest` operator waits until all observables emit `true`,
1139
+ * then `take(1)` ensures the hook is called only once, and `switchMap` handles
1140
+ * the hook execution.
1141
+ *
1142
+ * @example
1143
+ * ```ts
1144
+ * // Called automatically in loadScene to setup the observer
1145
+ * this.setupOnAfterLoadingObserver();
1146
+ * ```
1147
+ */
1148
+ private setupOnAfterLoadingObserver(): void {
1149
+ this.onAfterLoadingSubscription = combineLatest([
1150
+ this.mapLoadCompleted$.pipe(filter(completed => completed === true)),
1151
+ this.playerIdReceived$.pipe(filter(received => received === true)),
1152
+ this.playersReceived$.pipe(filter(received => received === true)),
1153
+ this.eventsReceived$.pipe(filter(received => received === true))
1154
+ ]).pipe(
1155
+ take(1), // Only execute once when all conditions are met
1156
+ switchMap(() => {
1157
+ // Call the hook and return the observable
1158
+ return this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap);
1159
+ })
1160
+ ).subscribe();
1161
+ }
1162
+
990
1163
  /**
991
1164
  * Clear client prediction states for cleanup
992
1165
  *
@@ -1,24 +1,30 @@
1
1
  <Container x={smoothX} y={smoothY} zIndex={y} viewportFollow={shouldFollowCamera} controls onBeforeDestroy visible >
2
- @for (component of componentsBehind) {
2
+ @for (compConfig of normalizedComponentsBehind) {
3
3
  <Container>
4
- <component object />
4
+ <compConfig.component object ...compConfig.props />
5
5
  </Container>
6
6
  }
7
7
  <Particle emit={@emitParticleTrigger} settings={@particleSettings} zIndex={1000} name={particleName} />
8
8
  <Container>
9
9
  @for (graphicObj of graphicsSignals) {
10
- <Sprite sheet={@sheet(@graphicObj)} direction tint hitbox flash={flashConfig} />
10
+ <Sprite
11
+ sheet={@sheet(@graphicObj)}
12
+ direction
13
+ tint
14
+ hitbox
15
+ flash={flashConfig}
16
+ />
11
17
  }
12
18
  </Container>
13
- @for (component of componentsInFront) {
14
- <Container>
15
- <component object />
19
+ @for (compConfig of normalizedComponentsInFront) {
20
+ <Container dependencies={@compConfig.@dependencies}>
21
+ <compConfig.component object ...compConfig.props />
16
22
  </Container>
17
- }
23
+ }
18
24
  @for (attachedGui of attachedGuis) {
19
25
  @if (shouldDisplayAttachedGui) {
20
26
  <Container>
21
- <attachedGui.component ...attachedGui.data() object={object} onFinish={(data) => {
27
+ <attachedGui.component ...attachedGui.data() dependencies={@attachedGui.@dependencies} object={object} onFinish={(data) => {
22
28
  onAttachedGuiFinish(attachedGui, data)
23
29
  }} onInteraction={(name, data) => {
24
30
  onAttachedGuiInteraction(attachedGui, name, data)
@@ -52,6 +58,137 @@
52
58
  const componentsBehind = client.spriteComponentsBehind;
53
59
  const componentsInFront = client.spriteComponentsInFront;
54
60
  const isMe = computed(() => id() === playerId);
61
+
62
+ /**
63
+ * Normalize a single sprite component configuration
64
+ *
65
+ * Handles both direct component references and configuration objects with optional props and dependencies.
66
+ * Extracts the component reference and creates a computed function that returns the props.
67
+ *
68
+ * ## Design
69
+ *
70
+ * Supports two formats:
71
+ * 1. Direct component: `ShadowComponent`
72
+ * 2. Configuration object: `{ component: LightHalo, props: {...}, dependencies: (object) => [...] }`
73
+ *
74
+ * The normalization process:
75
+ * - Extracts the actual component from either format
76
+ * - Extracts dependencies function if provided
77
+ * - Creates a computed function that returns props (static object or dynamic function result)
78
+ * - Returns a normalized object with `component`, `props`, and `dependencies`
79
+ *
80
+ * @param comp - Component reference or configuration object
81
+ * @returns Normalized component configuration with component, props, and dependencies
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // Direct component
86
+ * normalizeComponent(ShadowComponent)
87
+ * // => { component: ShadowComponent, props: {}, dependencies: undefined }
88
+ *
89
+ * // With static props
90
+ * normalizeComponent({ component: LightHalo, props: { radius: 30 } })
91
+ * // => { component: LightHalo, props: { radius: 30 }, dependencies: undefined }
92
+ *
93
+ * // With dynamic props and dependencies
94
+ * normalizeComponent({
95
+ * component: HealthBar,
96
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
97
+ * dependencies: (object) => [object.hp, object.param.maxHp]
98
+ * })
99
+ * // => { component: HealthBar, props: {...}, dependencies: (object) => [...] }
100
+ * ```
101
+ */
102
+ const normalizeComponent = (comp) => {
103
+ let componentRef;
104
+ let propsValue;
105
+ let dependenciesFn;
106
+
107
+ // If it's a direct component reference
108
+ if (typeof comp === 'function' || (comp && typeof comp === 'object' && !comp.component)) {
109
+ componentRef = comp;
110
+ propsValue = undefined;
111
+ dependenciesFn = undefined;
112
+ }
113
+ // If it's a configuration object with component and props
114
+ else if (comp && typeof comp === 'object' && comp.component) {
115
+ componentRef = comp.component;
116
+ // Support both "data" (legacy) and "props" (new) for backward compatibility
117
+ propsValue = comp.props !== undefined ? comp.props : comp.data;
118
+ dependenciesFn = comp.dependencies;
119
+ }
120
+ // Fallback: treat as direct component
121
+ else {
122
+ componentRef = comp;
123
+ propsValue = undefined;
124
+ dependenciesFn = undefined;
125
+ }
126
+
127
+ // Return props directly (object or function), not as computed
128
+ // The computed will be created in the template when needed
129
+ return {
130
+ component: componentRef,
131
+ props: typeof propsValue === 'function' ? propsValue(object) : propsValue || {},
132
+ dependencies: dependenciesFn ? dependenciesFn(object) : []
133
+ };
134
+ };
135
+
136
+ /**
137
+ * Normalize an array of sprite components
138
+ *
139
+ * Applies normalization to each component in the array using `normalizeComponent`.
140
+ *
141
+ * @param components - Array of component references or configuration objects
142
+ * @returns Array of normalized component configurations
143
+ */
144
+ const normalizeComponents = (components) => {
145
+ return components.map((comp) => normalizeComponent(comp));
146
+ };
147
+
148
+
149
+ /**
150
+ * Normalized components to render behind sprites
151
+ * Handles both direct component references and configuration objects with optional props and dependencies
152
+ *
153
+ * Supports multiple formats:
154
+ * 1. Direct component: `ShadowComponent`
155
+ * 2. Configuration object: `{ component: LightHalo, props: {...} }`
156
+ * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
157
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
158
+ *
159
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
160
+ * The object is passed to the dependencies function to allow sprite-specific dependency resolution.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * // Direct component
165
+ * componentsBehind: [ShadowComponent]
166
+ *
167
+ * // With static props
168
+ * componentsBehind: [{ component: LightHalo, props: { radius: 30 } }]
169
+ *
170
+ * // With dynamic props and dependencies
171
+ * componentsBehind: [{
172
+ * component: HealthBar,
173
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
174
+ * dependencies: (object) => [object.hp, object.param.maxHp]
175
+ * }]
176
+ * ```
177
+ */
178
+ const normalizedComponentsBehind = computed(() => {
179
+ return normalizeComponents(componentsBehind());
180
+ });
181
+
182
+ /**
183
+ * Normalized components to render in front of sprites
184
+ * Handles both direct component references and configuration objects with optional props and dependencies
185
+ *
186
+ * See `normalizedComponentsBehind` for format details.
187
+ * Components with dependencies will only be displayed when all dependencies are resolved.
188
+ */
189
+ const normalizedComponentsInFront = computed(() => {
190
+ return normalizeComponents(componentsInFront());
191
+ });
55
192
 
56
193
  /**
57
194
  * Determine if the camera should follow this sprite
@@ -294,7 +431,7 @@
294
431
  mount((element) => {
295
432
  hooks.callHooks("client-sprite-onAdd", object).subscribe()
296
433
  hooks.callHooks("client-sceneMap-onAddSprite", client.sceneMap, object).subscribe()
297
- client.setKeyboardControls(element.directives.controls)
434
+ if (isMe()) client.setKeyboardControls(element.directives.controls)
298
435
  })
299
436
 
300
437
  /**
@@ -0,0 +1,24 @@
1
+ import { inject } from "../../../core/inject";
2
+ import { RpgClientEngine } from "../../../RpgClientEngine";
3
+ import MobileGui from "./mobile.ce";
4
+ import { signal } from "canvasengine";
5
+
6
+ function isMobile() {
7
+ return /Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry/i.test(navigator.userAgent);
8
+ }
9
+
10
+ export const withMobile = () => (
11
+ {
12
+ gui: [
13
+ {
14
+ id: 'mobile-gui',
15
+ component: MobileGui,
16
+ autoDisplay: true,
17
+ dependencies: () => {
18
+ const engine = inject(RpgClientEngine);
19
+ return [signal(isMobile() ||undefined), engine.controlsReady]
20
+ }
21
+ }
22
+ ]
23
+ }
24
+ )