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

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 (135) hide show
  1. package/dist/Game/Object.d.ts +111 -0
  2. package/dist/Game/TransitionManager.d.ts +56 -0
  3. package/dist/RpgClientEngine.d.ts +306 -9
  4. package/dist/components/gui/mobile/index.d.ts +8 -0
  5. package/dist/components/prebuilt/index.d.ts +1 -0
  6. package/dist/index.d.ts +7 -1
  7. package/dist/index.js +14 -8
  8. package/dist/index.js.map +1 -1
  9. package/dist/index10.js +1 -1
  10. package/dist/index11.js +6 -5
  11. package/dist/index11.js.map +1 -1
  12. package/dist/index12.js +2 -2
  13. package/dist/index13.js +102 -10
  14. package/dist/index13.js.map +1 -1
  15. package/dist/index14.js +68 -9
  16. package/dist/index14.js.map +1 -1
  17. package/dist/index15.js +10 -224
  18. package/dist/index15.js.map +1 -1
  19. package/dist/index16.js +9 -97
  20. package/dist/index16.js.map +1 -1
  21. package/dist/index17.js +300 -89
  22. package/dist/index17.js.map +1 -1
  23. package/dist/index18.js +63 -80
  24. package/dist/index18.js.map +1 -1
  25. package/dist/index19.js +96 -348
  26. package/dist/index19.js.map +1 -1
  27. package/dist/index2.js +387 -21
  28. package/dist/index2.js.map +1 -1
  29. package/dist/index20.js +361 -5
  30. package/dist/index20.js.map +1 -1
  31. package/dist/index21.js +19 -50
  32. package/dist/index21.js.map +1 -1
  33. package/dist/index22.js +212 -5
  34. package/dist/index22.js.map +1 -1
  35. package/dist/index23.js +6 -395
  36. package/dist/index23.js.map +1 -1
  37. package/dist/index24.js +4 -39
  38. package/dist/index24.js.map +1 -1
  39. package/dist/index25.js +19 -20
  40. package/dist/index25.js.map +1 -1
  41. package/dist/index26.js +44 -2624
  42. package/dist/index26.js.map +1 -1
  43. package/dist/index27.js +5 -110
  44. package/dist/index27.js.map +1 -1
  45. package/dist/index28.js +394 -65
  46. package/dist/index28.js.map +1 -1
  47. package/dist/index29.js +40 -15
  48. package/dist/index29.js.map +1 -1
  49. package/dist/index3.js +3 -3
  50. package/dist/index30.js +21 -23
  51. package/dist/index30.js.map +1 -1
  52. package/dist/index31.js +49 -91
  53. package/dist/index31.js.map +1 -1
  54. package/dist/index32.js +2624 -32
  55. package/dist/index32.js.map +1 -1
  56. package/dist/index33.js +108 -18
  57. package/dist/index33.js.map +1 -1
  58. package/dist/index34.js +69 -3
  59. package/dist/index34.js.map +1 -1
  60. package/dist/index35.js +17 -331
  61. package/dist/index35.js.map +1 -1
  62. package/dist/index36.js +24 -24
  63. package/dist/index36.js.map +1 -1
  64. package/dist/index37.js +92 -8
  65. package/dist/index37.js.map +1 -1
  66. package/dist/index38.js +37 -7
  67. package/dist/index38.js.map +1 -1
  68. package/dist/index39.js +22 -10
  69. package/dist/index39.js.map +1 -1
  70. package/dist/index4.js +3 -3
  71. package/dist/index40.js +140 -6
  72. package/dist/index40.js.map +1 -1
  73. package/dist/index41.js +31 -3678
  74. package/dist/index41.js.map +1 -1
  75. package/dist/index42.js +3 -185
  76. package/dist/index42.js.map +1 -1
  77. package/dist/index43.js +172 -489
  78. package/dist/index43.js.map +1 -1
  79. package/dist/index44.js +498 -71
  80. package/dist/index44.js.map +1 -1
  81. package/dist/index45.js +331 -2
  82. package/dist/index45.js.map +1 -1
  83. package/dist/index46.js +25 -11
  84. package/dist/index46.js.map +1 -1
  85. package/dist/index47.js +70 -139
  86. package/dist/index47.js.map +1 -1
  87. package/dist/index48.js +9 -9
  88. package/dist/index48.js.map +1 -1
  89. package/dist/index49.js +6 -112
  90. package/dist/index49.js.map +1 -1
  91. package/dist/index5.js +1 -1
  92. package/dist/index50.js +3678 -124
  93. package/dist/index50.js.map +1 -1
  94. package/dist/index51.js +48 -131
  95. package/dist/index51.js.map +1 -1
  96. package/dist/index52.js +17 -109
  97. package/dist/index52.js.map +1 -1
  98. package/dist/index53.js +3 -138
  99. package/dist/index53.js.map +1 -1
  100. package/dist/index54.js +10 -7
  101. package/dist/index54.js.map +1 -1
  102. package/dist/index55.js +107 -48
  103. package/dist/index55.js.map +1 -1
  104. package/dist/index56.js +136 -0
  105. package/dist/index56.js.map +1 -0
  106. package/dist/index57.js +137 -0
  107. package/dist/index57.js.map +1 -0
  108. package/dist/index58.js +112 -0
  109. package/dist/index58.js.map +1 -0
  110. package/dist/index59.js +9 -0
  111. package/dist/index59.js.map +1 -0
  112. package/dist/index6.js +1 -1
  113. package/dist/index7.js +1 -1
  114. package/dist/index8.js +20 -2
  115. package/dist/index8.js.map +1 -1
  116. package/dist/index9.js +11 -27
  117. package/dist/index9.js.map +1 -1
  118. package/dist/module.d.ts +43 -4
  119. package/dist/services/keyboardControls.d.ts +11 -1
  120. package/package.json +11 -10
  121. package/src/Game/Object.ts +90 -8
  122. package/src/Game/TransitionManager.ts +75 -0
  123. package/src/Gui/Gui.ts +5 -31
  124. package/src/RpgClientEngine.ts +430 -16
  125. package/src/components/character.ce +212 -11
  126. package/src/components/gui/mobile/index.ts +24 -0
  127. package/src/components/gui/mobile/mobile.ce +95 -0
  128. package/src/components/prebuilt/index.ts +2 -0
  129. package/src/components/prebuilt/light-halo.ce +217 -0
  130. package/src/components/scenes/canvas.ce +12 -2
  131. package/src/components/scenes/draw-map.ce +12 -3
  132. package/src/components/scenes/transition.ce +60 -0
  133. package/src/index.ts +7 -1
  134. package/src/module.ts +66 -2
  135. package/src/services/keyboardControls.ts +14 -2
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,9 @@ 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);
321
+ console.log(guiInstance.dependencies)
322
322
  }
323
323
  }
324
324
 
@@ -371,33 +371,7 @@ export class RpgGui {
371
371
  * @param guiInstance - GUI instance
372
372
  */
373
373
  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);
374
+
401
375
  }
402
376
 
403
377
  /**
@@ -1,6 +1,6 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
- import { Context, inject } from "@signe/di";
3
- import { signal, bootstrapCanvas, Howler, Howl } from "canvasengine";
2
+ import { inject } from './core/inject'
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";
6
6
  import { RpgSound } from "./Sound";
@@ -12,6 +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";
15
16
  import { lastValueFrom, Observable } from "rxjs";
16
17
  import { GlobalConfigToken } from "./module";
17
18
  import * as PIXI from "pixi.js";
@@ -20,7 +21,6 @@ import {
20
21
  PredictionController,
21
22
  type PredictionState,
22
23
  } from "@rpgjs/common";
23
- import { KeyboardControls } from "./services/keyboardControls";
24
24
 
25
25
  export class RpgClientEngine<T = any> {
26
26
  private guiService: RpgGui;
@@ -37,6 +37,7 @@ export class RpgClientEngine<T = any> {
37
37
  spritesheets: Map<string, any> = new Map();
38
38
  sounds: Map<string, any> = new Map();
39
39
  componentAnimations: any[] = [];
40
+ transitions: any[] = [];
40
41
  private spritesheetResolver?: (id: string) => any | Promise<any>;
41
42
  private soundResolver?: (id: string) => any | Promise<any>;
42
43
  particleSettings: {
@@ -49,6 +50,10 @@ export class RpgClientEngine<T = any> {
49
50
  playerIdSignal = signal<string | null>(null);
50
51
  spriteComponentsBehind = signal<any[]>([]);
51
52
  spriteComponentsInFront = signal<any[]>([]);
53
+ /** ID of the sprite that the camera should follow. null means follow the current player */
54
+ cameraFollowTargetId = signal<string | null>(null);
55
+ /** Trigger for map shake animation */
56
+ mapShakeTrigger = trigger();
52
57
 
53
58
  private predictionEnabled = false;
54
59
  private prediction?: PredictionController<Direction>;
@@ -61,12 +66,12 @@ export class RpgClientEngine<T = any> {
61
66
  private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
62
67
  private lastInputTime = 0;
63
68
 
64
- constructor(public context: Context) {
65
- this.webSocket = inject(context, WebSocketToken);
66
- this.guiService = inject(context, RpgGui);
67
- this.loadMapService = inject(context, LoadMapToken);
68
- this.hooks = inject<Hooks>(context, ModulesToken);
69
- this.globalConfig = inject(context, GlobalConfigToken)
69
+ constructor(public context) {
70
+ this.webSocket = inject(WebSocketToken);
71
+ this.guiService = inject(RpgGui);
72
+ this.loadMapService = inject(LoadMapToken);
73
+ this.hooks = inject<Hooks>(ModulesToken);
74
+ this.globalConfig = inject(GlobalConfigToken)
70
75
 
71
76
  if (!this.globalConfig) {
72
77
  this.globalConfig = {} as T
@@ -124,8 +129,8 @@ export class RpgClientEngine<T = any> {
124
129
  * ```
125
130
  */
126
131
  setKeyboardControls(controlInstance: any) {
127
- const currentValues = this.context.values['inject:' + KeyboardControls]
128
- this.context.values['inject:' + KeyboardControls] = {
132
+ const currentValues = this.context.values['inject:' + 'KeyboardControls']
133
+ this.context.values['inject:' + 'KeyboardControls'] = {
129
134
  ...currentValues,
130
135
  values: new Map([['__default__', controlInstance]])
131
136
  }
@@ -158,6 +163,7 @@ export class RpgClientEngine<T = any> {
158
163
  this.hooks.callHooks("client-gui-load", this).subscribe();
159
164
  this.hooks.callHooks("client-particles-load", this).subscribe();
160
165
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
166
+ this.hooks.callHooks("client-transitions-load", this).subscribe();
161
167
  this.hooks.callHooks("client-sprite-load", this).subscribe();
162
168
 
163
169
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
@@ -214,6 +220,8 @@ export class RpgClientEngine<T = any> {
214
220
 
215
221
  this.webSocket.on("changeMap", (data) => {
216
222
  this.sceneMap.reset()
223
+ // Reset camera follow to default (follow current player) when changing maps
224
+ this.cameraFollowTargetId.set(null);
217
225
  this.loadScene(data.mapId);
218
226
  });
219
227
 
@@ -242,6 +250,33 @@ export class RpgClientEngine<T = any> {
242
250
  this.stopSound(soundId);
243
251
  });
244
252
 
253
+ this.webSocket.on("stopAllSounds", () => {
254
+ this.stopAllSounds();
255
+ });
256
+
257
+ this.webSocket.on("cameraFollow", (data) => {
258
+ const { targetId, smoothMove } = data;
259
+ this.setCameraFollow(targetId, smoothMove);
260
+ });
261
+
262
+ this.webSocket.on("flash", (data) => {
263
+ const { object, type, duration, cycles, alpha, tint } = data;
264
+ const sprite = object ? this.sceneMap.getObjectById(object) : undefined;
265
+ if (sprite && typeof sprite.flash === 'function') {
266
+ sprite.flash({ type, duration, cycles, alpha, tint });
267
+ }
268
+ });
269
+
270
+ this.webSocket.on("shakeMap", (data) => {
271
+ const { intensity, duration, frequency, direction } = data || {};
272
+ this.mapShakeTrigger.start({
273
+ intensity,
274
+ duration,
275
+ frequency,
276
+ direction
277
+ });
278
+ });
279
+
245
280
  this.webSocket.on('open', () => {
246
281
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
247
282
  // Start ping/pong for synchronization
@@ -673,6 +708,79 @@ export class RpgClientEngine<T = any> {
673
708
  }
674
709
  }
675
710
 
711
+ /**
712
+ * Stop all currently playing sounds
713
+ *
714
+ * This method stops all sounds that are currently playing.
715
+ * Useful when changing maps to prevent sound overlap.
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * // Stop all sounds
720
+ * engine.stopAllSounds();
721
+ * ```
722
+ */
723
+ stopAllSounds(): void {
724
+ this.sounds.forEach((sound) => {
725
+ if (sound && sound.stop) {
726
+ sound.stop();
727
+ }
728
+ });
729
+ }
730
+
731
+ /**
732
+ * Set the camera to follow a specific sprite
733
+ *
734
+ * This method changes which sprite the camera viewport should follow.
735
+ * The camera will smoothly animate to the target sprite if smoothMove options are provided.
736
+ *
737
+ * ## Design
738
+ *
739
+ * The camera follow target is stored in a signal that is read by sprite components.
740
+ * Each sprite checks if it should be followed by comparing its ID with the target ID.
741
+ * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's
742
+ * viewport system.
743
+ *
744
+ * @param targetId - The ID of the sprite to follow. Set to null to follow the current player
745
+ * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease
746
+ * @param smoothMove.time - Duration of the animation in milliseconds (optional)
747
+ * @param smoothMove.ease - Easing function name from https://easings.net (optional)
748
+ *
749
+ * @example
750
+ * ```ts
751
+ * // Follow another player with default smooth animation
752
+ * engine.setCameraFollow(otherPlayerId, true);
753
+ *
754
+ * // Follow an event with custom smooth animation
755
+ * engine.setCameraFollow(eventId, {
756
+ * time: 1000,
757
+ * ease: "easeInOutQuad"
758
+ * });
759
+ *
760
+ * // Follow without animation (instant)
761
+ * engine.setCameraFollow(targetId, false);
762
+ *
763
+ * // Return to following current player
764
+ * engine.setCameraFollow(null);
765
+ * ```
766
+ */
767
+ setCameraFollow(
768
+ targetId: string | null,
769
+ smoothMove?: boolean | { time?: number; ease?: string }
770
+ ): void {
771
+ // Store smoothMove options for potential future use with viewport animation
772
+ // For now, we just set the target ID and let CanvasEngine handle the viewport follow
773
+ // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it
774
+ this.cameraFollowTargetId.set(targetId);
775
+
776
+ // If smoothMove is an object, we could store it for viewport configuration
777
+ // This would require integration with CanvasEngine's viewport animation system
778
+ if (typeof smoothMove === "object" && smoothMove !== null) {
779
+ // Future: Apply smoothMove.time and smoothMove.ease to viewport animation
780
+ // For now, CanvasEngine handles viewport following automatically
781
+ }
782
+ }
783
+
676
784
  addParticle(particle: any) {
677
785
  this.particleSettings.emitters.push(particle)
678
786
  return particle;
@@ -682,13 +790,38 @@ export class RpgClientEngine<T = any> {
682
790
  * Add a component to render behind sprites
683
791
  * Components added with this method will be displayed with a lower z-index than the sprite
684
792
  *
685
- * @param component - The component to add behind sprites
686
- * @returns The added component
793
+ * Supports multiple formats:
794
+ * 1. Direct component: `ShadowComponent`
795
+ * 2. Configuration object: `{ component: LightHalo, props: {...} }`
796
+ * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
797
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
798
+ *
799
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
800
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
801
+ *
802
+ * @param component - The component to add behind sprites, or a configuration object
803
+ * @param component.component - The component function to render
804
+ * @param component.props - Static props object or function that receives the sprite object and returns props
805
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
806
+ * @returns The added component or configuration
687
807
  *
688
808
  * @example
689
809
  * ```ts
690
810
  * // Add a shadow component behind all sprites
691
811
  * engine.addSpriteComponentBehind(ShadowComponent);
812
+ *
813
+ * // Add a component with static props
814
+ * engine.addSpriteComponentBehind({
815
+ * component: LightHalo,
816
+ * props: { radius: 30 }
817
+ * });
818
+ *
819
+ * // Add a component with dynamic props and dependencies
820
+ * engine.addSpriteComponentBehind({
821
+ * component: HealthBar,
822
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
823
+ * dependencies: (object) => [object.hp, object.param.maxHp]
824
+ * });
692
825
  * ```
693
826
  */
694
827
  addSpriteComponentBehind(component: any) {
@@ -700,16 +833,41 @@ export class RpgClientEngine<T = any> {
700
833
  * Add a component to render in front of sprites
701
834
  * Components added with this method will be displayed with a higher z-index than the sprite
702
835
  *
703
- * @param component - The component to add in front of sprites
704
- * @returns The added component
836
+ * Supports multiple formats:
837
+ * 1. Direct component: `HealthBarComponent`
838
+ * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`
839
+ * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`
840
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
841
+ *
842
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
843
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
844
+ *
845
+ * @param component - The component to add in front of sprites, or a configuration object
846
+ * @param component.component - The component function to render
847
+ * @param component.props - Static props object or function that receives the sprite object and returns props
848
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
849
+ * @returns The added component or configuration
705
850
  *
706
851
  * @example
707
852
  * ```ts
708
853
  * // Add a health bar component in front of all sprites
709
854
  * engine.addSpriteComponentInFront(HealthBarComponent);
855
+ *
856
+ * // Add a component with static props
857
+ * engine.addSpriteComponentInFront({
858
+ * component: StatusIndicator,
859
+ * props: { type: 'poison' }
860
+ * });
861
+ *
862
+ * // Add a component with dynamic props and dependencies
863
+ * engine.addSpriteComponentInFront({
864
+ * component: HealthBar,
865
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
866
+ * dependencies: (object) => [object.hp, object.param.maxHp]
867
+ * });
710
868
  * ```
711
869
  */
712
- addSpriteComponentInFront(component: any) {
870
+ addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {
713
871
  this.spriteComponentsInFront.update((components: any[]) => [...components, component])
714
872
  return component
715
873
  }
@@ -779,6 +937,196 @@ export class RpgClientEngine<T = any> {
779
937
  return componentAnimation.instance
780
938
  }
781
939
 
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
+ /**
1083
+ * Start a transition
1084
+ *
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.
1088
+ *
1089
+ * @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
1092
+ *
1093
+ * @example
1094
+ * ```ts
1095
+ * // Start a fade transition
1096
+ * engine.startTransition('fade', { duration: 1000, color: 'black' });
1097
+ *
1098
+ * // Start with onFinish callback
1099
+ * engine.startTransition('fade', {
1100
+ * duration: 1000,
1101
+ * onFinish: () => console.log('Fade complete')
1102
+ * });
1103
+ * ```
1104
+ */
1105
+ 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
+ }
1119
+ }
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);
1128
+ }
1129
+
782
1130
  async processInput({ input }: { input: Direction }) {
783
1131
  const timestamp = Date.now();
784
1132
  let frame: number;
@@ -900,6 +1248,72 @@ export class RpgClientEngine<T = any> {
900
1248
  this.inputFrameCounter = 0;
901
1249
  }
902
1250
 
1251
+ /**
1252
+ * Trigger a flash animation on a sprite
1253
+ *
1254
+ * This method allows you to trigger a flash effect on any sprite from client-side code.
1255
+ * The flash can be configured with various options including type (alpha, tint, or both),
1256
+ * duration, cycles, and color.
1257
+ *
1258
+ * ## Design
1259
+ *
1260
+ * The flash is applied directly to the sprite object using its flash trigger.
1261
+ * This is useful for client-side visual feedback, UI interactions, or local effects
1262
+ * that don't need to be synchronized with the server.
1263
+ *
1264
+ * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player
1265
+ * @param options - Flash configuration options
1266
+ * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')
1267
+ * @param options.duration - Duration of the flash animation in milliseconds (default: 300)
1268
+ * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)
1269
+ * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)
1270
+ * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)
1271
+ *
1272
+ * @example
1273
+ * ```ts
1274
+ * // Flash the current player with default settings
1275
+ * engine.flash();
1276
+ *
1277
+ * // Flash a specific sprite with red tint
1278
+ * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });
1279
+ *
1280
+ * // Flash with both alpha and tint for dramatic effect
1281
+ * engine.flash(undefined, {
1282
+ * type: 'both',
1283
+ * alpha: 0.5,
1284
+ * tint: 0xff0000,
1285
+ * duration: 200,
1286
+ * cycles: 2
1287
+ * });
1288
+ *
1289
+ * // Quick damage flash on current player
1290
+ * engine.flash(undefined, {
1291
+ * type: 'tint',
1292
+ * tint: 'red',
1293
+ * duration: 150,
1294
+ * cycles: 1
1295
+ * });
1296
+ * ```
1297
+ */
1298
+ flash(
1299
+ spriteId?: string,
1300
+ options?: {
1301
+ type?: 'alpha' | 'tint' | 'both';
1302
+ duration?: number;
1303
+ cycles?: number;
1304
+ alpha?: number;
1305
+ tint?: number | string;
1306
+ }
1307
+ ): void {
1308
+ const targetId = spriteId || this.playerId;
1309
+ if (!targetId) return;
1310
+
1311
+ const sprite = this.sceneMap.getObjectById(targetId);
1312
+ if (sprite && typeof sprite.flash === 'function') {
1313
+ sprite.flash(options);
1314
+ }
1315
+ }
1316
+
903
1317
  private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
904
1318
  if (this.predictionEnabled && this.prediction) {
905
1319
  this.prediction.applyServerAck({