@rpgjs/client 5.0.0-beta.3 → 5.0.0-beta.4

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.
@@ -5,6 +5,10 @@ type Frame = {
5
5
  y: number;
6
6
  ts: number;
7
7
  };
8
+ type AnimationRestoreOptions = {
9
+ restoreAnimationName?: string;
10
+ restoreGraphics?: any[];
11
+ };
8
12
  export declare abstract class RpgClientObject extends RpgCommonPlayer {
9
13
  abstract _type: string;
10
14
  emitParticleTrigger: import('canvasengine').Trigger<any>;
@@ -16,6 +20,7 @@ export declare abstract class RpgClientObject extends RpgCommonPlayer {
16
20
  graphicsSignals: import('canvasengine').WritableArraySignal<any[]>;
17
21
  _component: {};
18
22
  flashTrigger: import('canvasengine').Trigger<any>;
23
+ private animationRestoreState?;
19
24
  constructor();
20
25
  /**
21
26
  * Access the shared client hook registry.
@@ -30,6 +35,9 @@ export declare abstract class RpgClientObject extends RpgCommonPlayer {
30
35
  */
31
36
  get engine(): RpgClientEngine<unknown>;
32
37
  private animationSubscription?;
38
+ private animationResetTimeout?;
39
+ private clearAnimationControls;
40
+ private finishTemporaryAnimation;
33
41
  /**
34
42
  * Trigger a flash animation on this sprite
35
43
  *
@@ -115,7 +123,7 @@ export declare abstract class RpgClientObject extends RpgCommonPlayer {
115
123
  * player.setAnimation('spell');
116
124
  * ```
117
125
  */
118
- setAnimation(animationName: string, nbTimes?: number): void;
126
+ setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): void;
119
127
  /**
120
128
  * Set a custom animation with temporary graphic change
121
129
  *
@@ -133,7 +141,7 @@ export declare abstract class RpgClientObject extends RpgCommonPlayer {
133
141
  * player.setAnimation('attack', 'hero_attack', 3);
134
142
  * ```
135
143
  */
136
- setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;
144
+ setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): void;
137
145
  /**
138
146
  * Display a registered component animation effect on this object.
139
147
  *
@@ -3,7 +3,7 @@ import component from "../components/dynamics/text.ce.js";
3
3
  import { RpgClientEngine } from "../RpgClientEngine.js";
4
4
  import { signal, trigger } from "canvasengine";
5
5
  import { ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
6
- import { filter, from, map, switchMap } from "rxjs";
6
+ import { filter, from, map, of, switchMap } from "rxjs";
7
7
  //#region src/Game/Object.ts
8
8
  var DYNAMIC_COMPONENTS = { text: component };
9
9
  var RpgClientObject = class extends RpgCommonPlayer {
@@ -24,7 +24,10 @@ var RpgClientObject = class extends RpgCommonPlayer {
24
24
  const nextFrames = items.flatMap((item) => Array.isArray(item) ? item : [item]);
25
25
  this.frames = [...this.frames, ...nextFrames];
26
26
  });
27
- this.graphics.observable.pipe(map(({ items }) => items), filter((graphics) => graphics.length > 0), switchMap((graphics) => from(Promise.all(graphics.map((graphic) => this.engine.getSpriteSheet(graphic)))))).subscribe((sheets) => {
27
+ this.graphics.observable.pipe(map(({ items }) => items), switchMap((graphics) => {
28
+ if (graphics.length === 0) return of([]);
29
+ return from(Promise.all(graphics.map((graphic) => this.engine.getSpriteSheet(graphic))));
30
+ })).subscribe((sheets) => {
28
31
  this.graphicsSignals.set(sheets);
29
32
  });
30
33
  this.componentsTop.observable.pipe(filter((value) => value !== null && value !== void 0), map((value) => typeof value === "string" ? JSON.parse(value) : value)).subscribe(({ components }) => {
@@ -59,6 +62,27 @@ var RpgClientObject = class extends RpgCommonPlayer {
59
62
  get engine() {
60
63
  return inject(RpgClientEngine);
61
64
  }
65
+ clearAnimationControls() {
66
+ if (this.animationSubscription) {
67
+ this.animationSubscription.unsubscribe();
68
+ this.animationSubscription = void 0;
69
+ }
70
+ if (this.animationResetTimeout) {
71
+ clearTimeout(this.animationResetTimeout);
72
+ this.animationResetTimeout = void 0;
73
+ }
74
+ }
75
+ finishTemporaryAnimation() {
76
+ const restoreState = this.animationRestoreState;
77
+ this.clearAnimationControls();
78
+ this.animationCurrentIndex.set(0);
79
+ if (restoreState) {
80
+ this.animationName.set(restoreState.animationName);
81
+ this.graphics.set([...restoreState.graphics]);
82
+ }
83
+ this.animationRestoreState = void 0;
84
+ this.animationIsPlaying.set(false);
85
+ }
62
86
  /**
63
87
  * Trigger a flash animation on this sprite
64
88
  *
@@ -142,41 +166,47 @@ var RpgClientObject = class extends RpgCommonPlayer {
142
166
  * ```
143
167
  */
144
168
  resetAnimationState() {
169
+ if (this.animationRestoreState) {
170
+ this.finishTemporaryAnimation();
171
+ return;
172
+ }
145
173
  this.animationIsPlaying.set(false);
146
174
  this.animationCurrentIndex.set(0);
147
- if (this.animationSubscription) {
148
- this.animationSubscription.unsubscribe();
149
- this.animationSubscription = void 0;
150
- }
175
+ this.clearAnimationControls();
151
176
  }
152
- setAnimation(animationName, graphicOrNbTimes, nbTimes) {
153
- if (this.animationIsPlaying()) return;
154
- this.animationIsPlaying.set(true);
155
- const previousAnimationName = this.animationName();
156
- const previousGraphics = this.graphics();
157
- this.animationCurrentIndex.set(0);
177
+ setAnimation(animationName, graphicOrNbTimes, nbTimesOrOptions, options) {
158
178
  let graphic;
159
179
  let finalNbTimes = Infinity;
160
- if (typeof graphicOrNbTimes === "number") finalNbTimes = graphicOrNbTimes;
161
- else if (graphicOrNbTimes !== void 0) {
180
+ let restoreOptions = options;
181
+ if (typeof graphicOrNbTimes === "number") {
182
+ finalNbTimes = graphicOrNbTimes;
183
+ restoreOptions = typeof nbTimesOrOptions === "object" ? nbTimesOrOptions : options;
184
+ } else if (graphicOrNbTimes !== void 0) {
162
185
  graphic = graphicOrNbTimes;
163
- finalNbTimes = nbTimes ?? Infinity;
186
+ if (typeof nbTimesOrOptions === "number") finalNbTimes = nbTimesOrOptions;
187
+ else {
188
+ finalNbTimes = Infinity;
189
+ restoreOptions = nbTimesOrOptions ?? options;
190
+ }
164
191
  } else finalNbTimes = Infinity;
192
+ if (this.animationIsPlaying()) this.finishTemporaryAnimation();
193
+ this.animationIsPlaying.set(true);
194
+ const previousAnimationName = restoreOptions?.restoreAnimationName ?? this.animationName();
195
+ const previousGraphics = restoreOptions?.restoreGraphics ? [...restoreOptions.restoreGraphics] : [...this.graphics()];
196
+ this.animationRestoreState = {
197
+ animationName: previousAnimationName,
198
+ graphics: previousGraphics
199
+ };
200
+ this.animationCurrentIndex.set(0);
165
201
  if (graphic !== void 0) if (Array.isArray(graphic)) this.graphics.set(graphic);
166
202
  else this.graphics.set([graphic]);
167
- if (this.animationSubscription) this.animationSubscription.unsubscribe();
203
+ this.clearAnimationControls();
168
204
  this.animationSubscription = this.animationCurrentIndex.observable.subscribe((index) => {
169
- if (index >= finalNbTimes) {
170
- this.animationCurrentIndex.set(0);
171
- this.animationName.set(previousAnimationName);
172
- if (graphic !== void 0) this.graphics.set(previousGraphics);
173
- this.animationIsPlaying.set(false);
174
- if (this.animationSubscription) {
175
- this.animationSubscription.unsubscribe();
176
- this.animationSubscription = void 0;
177
- }
178
- }
205
+ if (index >= finalNbTimes) this.finishTemporaryAnimation();
179
206
  });
207
+ if (finalNbTimes !== Infinity) this.animationResetTimeout = setTimeout(() => {
208
+ if (this.animationIsPlaying()) this.finishTemporaryAnimation();
209
+ }, Math.max(1e3, finalNbTimes * 1e3));
180
210
  this.animationName.set(animationName);
181
211
  }
182
212
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Object.js","names":[],"sources":["../../src/Game/Object.ts"],"sourcesContent":["import { Hooks, ModulesToken, RpgCommonPlayer } from \"@rpgjs/common\";\nimport { trigger, signal, effect } from \"canvasengine\";\nimport { filter, from, map, Subscription, switchMap } from \"rxjs\";\nimport { inject } from \"../core/inject\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport TextComponent from \"../components/dynamics/text.ce\";\n\nconst DYNAMIC_COMPONENTS = {\n text: TextComponent,\n}\n\ntype Frame = { x: number; y: number; ts: number };\n\nexport abstract class RpgClientObject extends RpgCommonPlayer {\n abstract _type: string;\n emitParticleTrigger = trigger();\n particleName = signal(\"\");\n animationCurrentIndex = signal(0);\n animationIsPlaying = signal(false);\n _param = signal({});\n frames: Frame[] = [];\n graphicsSignals = signal<any[]>([]);\n _component = {} // temporary component memory\n flashTrigger = trigger();\n\n constructor() {\n super();\n this.hooks.callHooks(\"client-sprite-onInit\", this).subscribe();\n\n this._frames.observable.subscribe(({ items }) => {\n if (!this.id) return;\n //if (this.id == this.engine.playerIdSignal()!) return;\n const nextFrames = items.flatMap((item): Frame[] =>\n Array.isArray(item) ? item : [item]\n );\n this.frames = [...this.frames, ...nextFrames];\n });\n\n this.graphics.observable\n .pipe(\n map(({ items }) => items),\n filter(graphics => graphics.length > 0),\n switchMap(graphics => from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic)))))\n )\n .subscribe((sheets) => { \n this.graphicsSignals.set(sheets);\n });\n\n this.componentsTop.observable\n .pipe(\n filter(value => value !== null && value !== undefined),\n map((value) => typeof value === 'string' ? JSON.parse(value) : value),\n )\n .subscribe(({components}) => {\n for (const component of components) {\n for (const [key, value] of Object.entries(component)) {\n this._component = value as any; // temporary component memory\n console.log(value)\n const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;\n if (DYNAMIC_COMPONENTS[type]) {\n this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);\n }\n }\n }\n });\n\n this.engine.tick\n .pipe\n //throttleTime(10)\n ()\n .subscribe(() => {\n const frame = this.frames.shift();\n if (frame) {\n if (typeof frame.x !== \"number\" || typeof frame.y !== \"number\") return;\n this.engine.scene.setBodyPosition(\n this.id,\n frame.x,\n frame.y,\n \"top-left\"\n );\n }\n });\n }\n\n /**\n * Access the shared client hook registry.\n *\n * @returns The hook service used to register and trigger client-side hooks.\n */\n get hooks() {\n return inject<Hooks>(ModulesToken);\n }\n\n /**\n * Access the current client engine instance.\n *\n * @returns The active {@link RpgClientEngine} instance.\n */\n get engine() {\n return inject(RpgClientEngine);\n }\n\n private animationSubscription?: Subscription;\n\n /**\n * Trigger a flash animation on this sprite\n * \n * This method triggers a flash effect using CanvasEngine's flash directive.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash uses a trigger system that is connected to the flash directive in the\n * character component. This allows for flexible configuration and can be triggered\n * from both server events and client-side code.\n * \n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Simple flash with default settings (alpha flash)\n * player.flash();\n * \n * // Flash with red tint\n * player.flash({ type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint\n * player.flash({ \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash\n * player.flash({ \n * type: 'tint', \n * tint: 0xff0000, \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }): void {\n const flashOptions = {\n type: options?.type || 'alpha',\n duration: options?.duration ?? 300,\n cycles: options?.cycles ?? 1,\n alpha: options?.alpha ?? 0.3,\n tint: options?.tint ?? 0xffffff,\n };\n \n // Convert color name to hex if needed\n let tintValue = flashOptions.tint;\n if (typeof tintValue === 'string') {\n // Common color name to hex mapping\n const colorMap: Record<string, number> = {\n 'white': 0xffffff,\n 'red': 0xff0000,\n 'green': 0x00ff00,\n 'blue': 0x0000ff,\n 'yellow': 0xffff00,\n 'cyan': 0x00ffff,\n 'magenta': 0xff00ff,\n 'black': 0x000000,\n };\n tintValue = colorMap[tintValue.toLowerCase()] ?? 0xffffff;\n }\n \n this.flashTrigger.start({\n ...flashOptions,\n tint: tintValue,\n });\n }\n\n /**\n * Reset animation state when animation changes externally\n *\n * This method should be called when the animation changes due to movement\n * or other external factors to ensure the animation system doesn't get stuck\n *\n * @example\n * ```ts\n * // Reset when player starts moving\n * player.resetAnimationState();\n * ```\n */\n resetAnimationState() {\n this.animationIsPlaying.set(false);\n this.animationCurrentIndex.set(0);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n\n /**\n * Set a custom animation for a specific number of times\n *\n * Plays a custom animation for the specified number of repetitions.\n * The animation system prevents overlapping animations and automatically\n * returns to the previous animation when complete.\n *\n * @param animationName - Name of the animation to play\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation 3 times\n * player.setAnimation('attack', 3);\n *\n * // Play continuous spell animation\n * player.setAnimation('spell');\n * ```\n */\n setAnimation(animationName: string, nbTimes?: number): void;\n /**\n * Set a custom animation with temporary graphic change\n *\n * Plays a custom animation for the specified number of repetitions and temporarily\n * changes the player's graphic (sprite sheet) during the animation. The graphic\n * is automatically reset when the animation finishes.\n *\n * @param animationName - Name of the animation to play\n * @param graphic - The graphic(s) to temporarily use during the animation\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation with temporary graphic change\n * player.setAnimation('attack', 'hero_attack', 3);\n * ```\n */\n setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;\n setAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes?: number): void {\n if (this.animationIsPlaying()) return;\n this.animationIsPlaying.set(true);\n const previousAnimationName = this.animationName();\n const previousGraphics = this.graphics();\n this.animationCurrentIndex.set(0);\n\n let graphic: string | string[] | undefined;\n let finalNbTimes: number = Infinity;\n\n // Handle overloads\n if (typeof graphicOrNbTimes === 'number') {\n // setAnimation(animationName, nbTimes)\n finalNbTimes = graphicOrNbTimes;\n } else if (graphicOrNbTimes !== undefined) {\n // setAnimation(animationName, graphic, nbTimes)\n graphic = graphicOrNbTimes;\n finalNbTimes = nbTimes ?? Infinity;\n } else {\n // setAnimation(animationName) - nbTimes remains Infinity\n finalNbTimes = Infinity;\n }\n\n // Temporarily change graphic if provided\n if (graphic !== undefined) {\n if (Array.isArray(graphic)) {\n this.graphics.set(graphic);\n } else {\n this.graphics.set([graphic]);\n }\n }\n\n // Clean up any existing subscription\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n }\n\n this.animationSubscription =\n this.animationCurrentIndex.observable.subscribe((index) => {\n if (index >= finalNbTimes) {\n this.animationCurrentIndex.set(0);\n this.animationName.set(previousAnimationName);\n // Reset graphic to previous value if it was changed\n if (graphic !== undefined) {\n this.graphics.set(previousGraphics);\n }\n this.animationIsPlaying.set(false);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n });\n this.animationName.set(animationName);\n }\n\n /**\n * Display a registered component animation effect on this object.\n *\n * @param id - Identifier of the component animation to play.\n * @param params - Parameters forwarded to the animation effect.\n */\n showComponentAnimation(id: string, params: any) {\n const engine = inject(RpgClientEngine);\n engine.getComponentAnimation(id).displayEffect(params, this);\n }\n \n /**\n * Check whether this client object represents an event.\n *\n * @returns `true` if the object type is `event`, otherwise `false`.\n */\n isEvent(): boolean {\n return this._type === 'event';\n }\n\n /**\n * Check whether this client object represents a player.\n *\n * @returns `true` if the object type is `player`, otherwise `false`.\n */\n isPlayer(): boolean {\n return this._type === 'player';\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Object.js","names":[],"sources":["../../src/Game/Object.ts"],"sourcesContent":["import { Hooks, ModulesToken, RpgCommonPlayer } from \"@rpgjs/common\";\nimport { trigger, signal, effect } from \"canvasengine\";\nimport { filter, from, map, of, Subscription, switchMap } from \"rxjs\";\nimport { inject } from \"../core/inject\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport TextComponent from \"../components/dynamics/text.ce\";\n\nconst DYNAMIC_COMPONENTS = {\n text: TextComponent,\n}\n\ntype Frame = { x: number; y: number; ts: number };\n\ntype AnimationRestoreOptions = {\n restoreAnimationName?: string;\n restoreGraphics?: any[];\n};\n\nexport abstract class RpgClientObject extends RpgCommonPlayer {\n abstract _type: string;\n emitParticleTrigger = trigger();\n particleName = signal(\"\");\n animationCurrentIndex = signal(0);\n animationIsPlaying = signal(false);\n _param = signal({});\n frames: Frame[] = [];\n graphicsSignals = signal<any[]>([]);\n _component = {} // temporary component memory\n flashTrigger = trigger();\n private animationRestoreState?: {\n animationName: string;\n graphics: any[];\n };\n\n constructor() {\n super();\n this.hooks.callHooks(\"client-sprite-onInit\", this).subscribe();\n\n this._frames.observable.subscribe(({ items }) => {\n if (!this.id) return;\n //if (this.id == this.engine.playerIdSignal()!) return;\n const nextFrames = items.flatMap((item): Frame[] =>\n Array.isArray(item) ? item : [item]\n );\n this.frames = [...this.frames, ...nextFrames];\n });\n\n this.graphics.observable\n .pipe(\n map(({ items }) => items),\n switchMap(graphics => {\n if (graphics.length === 0) return of([]);\n return from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic))));\n })\n )\n .subscribe((sheets) => { \n this.graphicsSignals.set(sheets);\n });\n\n this.componentsTop.observable\n .pipe(\n filter(value => value !== null && value !== undefined),\n map((value) => typeof value === 'string' ? JSON.parse(value) : value),\n )\n .subscribe(({components}) => {\n for (const component of components) {\n for (const [key, value] of Object.entries(component)) {\n this._component = value as any; // temporary component memory\n console.log(value)\n const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;\n if (DYNAMIC_COMPONENTS[type]) {\n this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);\n }\n }\n }\n });\n\n this.engine.tick\n .pipe\n //throttleTime(10)\n ()\n .subscribe(() => {\n const frame = this.frames.shift();\n if (frame) {\n if (typeof frame.x !== \"number\" || typeof frame.y !== \"number\") return;\n this.engine.scene.setBodyPosition(\n this.id,\n frame.x,\n frame.y,\n \"top-left\"\n );\n }\n });\n }\n\n /**\n * Access the shared client hook registry.\n *\n * @returns The hook service used to register and trigger client-side hooks.\n */\n get hooks() {\n return inject<Hooks>(ModulesToken);\n }\n\n /**\n * Access the current client engine instance.\n *\n * @returns The active {@link RpgClientEngine} instance.\n */\n get engine() {\n return inject(RpgClientEngine);\n }\n\n private animationSubscription?: Subscription;\n private animationResetTimeout?: ReturnType<typeof setTimeout>;\n\n private clearAnimationControls() {\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n if (this.animationResetTimeout) {\n clearTimeout(this.animationResetTimeout);\n this.animationResetTimeout = undefined;\n }\n }\n\n private finishTemporaryAnimation() {\n const restoreState = this.animationRestoreState;\n this.clearAnimationControls();\n this.animationCurrentIndex.set(0);\n if (restoreState) {\n this.animationName.set(restoreState.animationName);\n this.graphics.set([...restoreState.graphics]);\n }\n this.animationRestoreState = undefined;\n this.animationIsPlaying.set(false);\n }\n\n /**\n * Trigger a flash animation on this sprite\n * \n * This method triggers a flash effect using CanvasEngine's flash directive.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash uses a trigger system that is connected to the flash directive in the\n * character component. This allows for flexible configuration and can be triggered\n * from both server events and client-side code.\n * \n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Simple flash with default settings (alpha flash)\n * player.flash();\n * \n * // Flash with red tint\n * player.flash({ type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint\n * player.flash({ \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash\n * player.flash({ \n * type: 'tint', \n * tint: 0xff0000, \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }): void {\n const flashOptions = {\n type: options?.type || 'alpha',\n duration: options?.duration ?? 300,\n cycles: options?.cycles ?? 1,\n alpha: options?.alpha ?? 0.3,\n tint: options?.tint ?? 0xffffff,\n };\n \n // Convert color name to hex if needed\n let tintValue = flashOptions.tint;\n if (typeof tintValue === 'string') {\n // Common color name to hex mapping\n const colorMap: Record<string, number> = {\n 'white': 0xffffff,\n 'red': 0xff0000,\n 'green': 0x00ff00,\n 'blue': 0x0000ff,\n 'yellow': 0xffff00,\n 'cyan': 0x00ffff,\n 'magenta': 0xff00ff,\n 'black': 0x000000,\n };\n tintValue = colorMap[tintValue.toLowerCase()] ?? 0xffffff;\n }\n \n this.flashTrigger.start({\n ...flashOptions,\n tint: tintValue,\n });\n }\n\n /**\n * Reset animation state when animation changes externally\n *\n * This method should be called when the animation changes due to movement\n * or other external factors to ensure the animation system doesn't get stuck\n *\n * @example\n * ```ts\n * // Reset when player starts moving\n * player.resetAnimationState();\n * ```\n */\n resetAnimationState() {\n if (this.animationRestoreState) {\n this.finishTemporaryAnimation();\n return;\n }\n this.animationIsPlaying.set(false);\n this.animationCurrentIndex.set(0);\n this.clearAnimationControls();\n }\n\n /**\n * Set a custom animation for a specific number of times\n *\n * Plays a custom animation for the specified number of repetitions.\n * The animation system prevents overlapping animations and automatically\n * returns to the previous animation when complete.\n *\n * @param animationName - Name of the animation to play\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation 3 times\n * player.setAnimation('attack', 3);\n *\n * // Play continuous spell animation\n * player.setAnimation('spell');\n * ```\n */\n setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): void;\n /**\n * Set a custom animation with temporary graphic change\n *\n * Plays a custom animation for the specified number of repetitions and temporarily\n * changes the player's graphic (sprite sheet) during the animation. The graphic\n * is automatically reset when the animation finishes.\n *\n * @param animationName - Name of the animation to play\n * @param graphic - The graphic(s) to temporarily use during the animation\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation with temporary graphic change\n * player.setAnimation('attack', 'hero_attack', 3);\n * ```\n */\n setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): void;\n setAnimation(\n animationName: string,\n graphicOrNbTimes?: string | string[] | number,\n nbTimesOrOptions?: number | AnimationRestoreOptions,\n options?: AnimationRestoreOptions\n ): void {\n let graphic: string | string[] | undefined;\n let finalNbTimes: number = Infinity;\n let restoreOptions: AnimationRestoreOptions | undefined = options;\n\n // Handle overloads\n if (typeof graphicOrNbTimes === 'number') {\n // setAnimation(animationName, nbTimes)\n finalNbTimes = graphicOrNbTimes;\n restoreOptions = typeof nbTimesOrOptions === 'object' ? nbTimesOrOptions : options;\n } else if (graphicOrNbTimes !== undefined) {\n // setAnimation(animationName, graphic, nbTimes)\n graphic = graphicOrNbTimes;\n if (typeof nbTimesOrOptions === 'number') {\n finalNbTimes = nbTimesOrOptions;\n } else {\n finalNbTimes = Infinity;\n restoreOptions = nbTimesOrOptions ?? options;\n }\n } else {\n // setAnimation(animationName) - nbTimes remains Infinity\n finalNbTimes = Infinity;\n }\n\n if (this.animationIsPlaying()) {\n this.finishTemporaryAnimation();\n }\n\n this.animationIsPlaying.set(true);\n const previousAnimationName =\n restoreOptions?.restoreAnimationName ?? this.animationName();\n const previousGraphics = restoreOptions?.restoreGraphics\n ? [...restoreOptions.restoreGraphics]\n : [...this.graphics()];\n this.animationRestoreState = {\n animationName: previousAnimationName,\n graphics: previousGraphics,\n };\n this.animationCurrentIndex.set(0);\n\n // Temporarily change graphic if provided\n if (graphic !== undefined) {\n if (Array.isArray(graphic)) {\n this.graphics.set(graphic);\n } else {\n this.graphics.set([graphic]);\n }\n }\n\n this.clearAnimationControls();\n\n this.animationSubscription =\n this.animationCurrentIndex.observable.subscribe((index) => {\n if (index >= finalNbTimes) {\n this.finishTemporaryAnimation();\n }\n });\n\n if (finalNbTimes !== Infinity) {\n this.animationResetTimeout = setTimeout(() => {\n if (this.animationIsPlaying()) {\n this.finishTemporaryAnimation();\n }\n }, Math.max(1000, finalNbTimes * 1000));\n }\n\n this.animationName.set(animationName);\n }\n\n /**\n * Display a registered component animation effect on this object.\n *\n * @param id - Identifier of the component animation to play.\n * @param params - Parameters forwarded to the animation effect.\n */\n showComponentAnimation(id: string, params: any) {\n const engine = inject(RpgClientEngine);\n engine.getComponentAnimation(id).displayEffect(params, this);\n }\n \n /**\n * Check whether this client object represents an event.\n *\n * @returns `true` if the object type is `event`, otherwise `false`.\n */\n isEvent(): boolean {\n return this._type === 'event';\n }\n\n /**\n * Check whether this client object represents a player.\n *\n * @returns `true` if the object type is `player`, otherwise `false`.\n */\n isPlayer(): boolean {\n return this._type === 'player';\n }\n}\n"],"mappings":"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"}
@@ -584,6 +584,22 @@ export declare class RpgClientEngine<T = any> {
584
584
  * ```
585
585
  */
586
586
  clearClientPredictionStates(): void;
587
+ /**
588
+ * Stop local movement immediately and discard pending predicted movement.
589
+ *
590
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
591
+ * startup, or any client-side state where already buffered movement inputs
592
+ * must not be replayed after server reconciliation.
593
+ *
594
+ * @param player - Player object to stop. Defaults to the current player.
595
+ * @returns `true` when a player was found and interrupted.
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * engine.interruptCurrentPlayerMovement();
600
+ * ```
601
+ */
602
+ interruptCurrentPlayerMovement(player?: any): boolean;
587
603
  /**
588
604
  * Trigger a flash animation on a sprite
589
605
  *
@@ -250,10 +250,15 @@ var RpgClientEngine = class {
250
250
  this.notificationManager.add(data);
251
251
  });
252
252
  this.webSocket.on("setAnimation", (data) => {
253
- const { animationName, nbTimes, object, graphic } = data;
254
- const player = this.sceneMap.getObjectById(object);
255
- if (graphic !== void 0) player.setAnimation(animationName, graphic, nbTimes);
256
- else player.setAnimation(animationName, nbTimes);
253
+ const { animationName, nbTimes, object, graphic, restoreAnimationName, restoreGraphics } = data;
254
+ const player = object ? this.sceneMap.getObjectById(object) : void 0;
255
+ if (!player) return;
256
+ const restoreOptions = {
257
+ restoreAnimationName,
258
+ restoreGraphics
259
+ };
260
+ if (graphic !== void 0) player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
261
+ else player.setAnimation(animationName, nbTimes, restoreOptions);
257
262
  });
258
263
  this.webSocket.on("playSound", (data) => {
259
264
  const { soundId, volume, loop } = data;
@@ -890,6 +895,12 @@ var RpgClientEngine = class {
890
895
  this.guiService.display(id, props);
891
896
  }
892
897
  async processInput({ input }) {
898
+ if (this.stopProcessingInput) return;
899
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
900
+ if (!(!currentPlayer || typeof currentPlayer.canMove !== "function" || currentPlayer.canMove())) {
901
+ this.interruptCurrentPlayerMovement(currentPlayer);
902
+ return;
903
+ }
893
904
  const timestamp = Date.now();
894
905
  let frame;
895
906
  let tick;
@@ -906,7 +917,6 @@ var RpgClientEngine = class {
906
917
  input,
907
918
  playerId: this.playerId
908
919
  }).subscribe();
909
- const currentPlayer = this.sceneMap.getCurrentPlayer();
910
920
  const bodyReady = this.ensureCurrentPlayerBody();
911
921
  if (currentPlayer && bodyReady) {
912
922
  currentPlayer.changeDirection(input);
@@ -921,6 +931,8 @@ var RpgClientEngine = class {
921
931
  }
922
932
  processAction({ action }) {
923
933
  if (this.stopProcessingInput) return;
934
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
935
+ if (!(!currentPlayer || typeof currentPlayer.canMove !== "function" || currentPlayer.canMove())) return;
924
936
  this.hooks.callHooks("client-engine-onInput", this, {
925
937
  input: "action",
926
938
  playerId: this.playerId
@@ -1009,6 +1021,11 @@ var RpgClientEngine = class {
1009
1021
  }
1010
1022
  flushPendingMovePath() {
1011
1023
  if (!this.predictionEnabled || !this.prediction) return;
1024
+ const player = this.sceneMap?.getCurrentPlayer?.();
1025
+ if (player && typeof player.canMove === "function" && !player.canMove()) {
1026
+ this.interruptCurrentPlayerMovement(player);
1027
+ return;
1028
+ }
1012
1029
  const pendingInputs = this.prediction.getPendingInputs();
1013
1030
  if (pendingInputs.length === 0) return;
1014
1031
  const latest = pendingInputs[pendingInputs.length - 1];
@@ -1125,6 +1142,31 @@ var RpgClientEngine = class {
1125
1142
  this.lastMovePathSentFrame = 0;
1126
1143
  }
1127
1144
  /**
1145
+ * Stop local movement immediately and discard pending predicted movement.
1146
+ *
1147
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
1148
+ * startup, or any client-side state where already buffered movement inputs
1149
+ * must not be replayed after server reconciliation.
1150
+ *
1151
+ * @param player - Player object to stop. Defaults to the current player.
1152
+ * @returns `true` when a player was found and interrupted.
1153
+ *
1154
+ * @example
1155
+ * ```ts
1156
+ * engine.interruptCurrentPlayerMovement();
1157
+ * ```
1158
+ */
1159
+ interruptCurrentPlayerMovement(player = this.sceneMap?.getCurrentPlayer?.()) {
1160
+ if (!player) return false;
1161
+ this.sceneMap?.stopMovement?.(player);
1162
+ this.prediction?.clearPendingInputs();
1163
+ this.pendingPredictionFrames = [];
1164
+ this.lastInputTime = 0;
1165
+ this.lastMovePathSentAt = Date.now();
1166
+ this.lastMovePathSentFrame = this.inputFrameCounter;
1167
+ return true;
1168
+ }
1169
+ /**
1128
1170
  * Trigger a flash animation on a sprite
1129
1171
  *
1130
1172
  * This method allows you to trigger a flash effect on any sprite from client-side code.
@@ -1206,6 +1248,10 @@ var RpgClientEngine = class {
1206
1248
  reconcilePrediction(authoritativeState, pendingInputs) {
1207
1249
  const player = this.getCurrentPlayer();
1208
1250
  if (!player) return;
1251
+ if (typeof player.canMove === "function" && !player.canMove()) {
1252
+ this.interruptCurrentPlayerMovement(player);
1253
+ return;
1254
+ }
1209
1255
  this.sceneMap.stopMovement(player);
1210
1256
  this.applyAuthoritativeState(authoritativeState);
1211
1257
  if (!pendingInputs.length) return;
@@ -1 +1 @@
1
- {"version":3,"file":"RpgClientEngine.js","names":[],"sources":["../src/RpgClientEngine.ts"],"sourcesContent":["import Canvas from \"./components/scenes/canvas.ce\";\nimport { inject } from './core/inject'\nimport { signal, bootstrapCanvas, Howl, trigger } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"./services/AbstractSocket\";\nimport { LoadMapService, LoadMapToken } from \"./services/loadMap\";\nimport { RpgSound } from \"./Sound\";\nimport { RpgResource } from \"./Resource\";\nimport { Hooks, ModulesToken, Direction } from \"@rpgjs/common\";\nimport { load } from \"@signe/sync\";\nimport { RpgClientMap } from \"./Game/Map\"\nimport { RpgGui } from \"./Gui/Gui\";\nimport { AnimationManager } from \"./Game/AnimationManager\";\nimport { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from \"rxjs\";\nimport { GlobalConfigToken } from \"./module\";\nimport * as PIXI from \"pixi.js\";\nimport { PrebuiltComponentAnimations } from \"./components/animations\";\nimport {\n PredictionController,\n type PredictionHistoryEntry,\n type PredictionState,\n} from \"@rpgjs/common\";\nimport { NotificationManager } from \"./Gui/NotificationManager\";\nimport { SaveClientService } from \"./services/save\";\n\ninterface MovementTrajectoryPoint {\n frame: number;\n tick: number;\n timestamp: number;\n input: Direction;\n x: number;\n y: number;\n direction?: Direction;\n}\n\nexport class RpgClientEngine<T = any> {\n private guiService: RpgGui;\n private webSocket: AbstractWebsocket;\n private loadMapService: LoadMapService;\n private hooks: Hooks;\n private sceneMap: RpgClientMap\n private selector: HTMLElement;\n public globalConfig: T;\n public sceneComponent: any;\n stopProcessingInput = false;\n width = signal(\"100%\");\n height = signal(\"100%\");\n spritesheets: Map<string, any> = new Map();\n sounds: Map<string, any> = new Map();\n componentAnimations: any[] = [];\n private spritesheetResolver?: (id: string) => any | Promise<any>;\n private soundResolver?: (id: string) => any | Promise<any>;\n particleSettings: {\n emitters: any[]\n } = {\n emitters: []\n }\n renderer: PIXI.Renderer;\n tick: Observable<number>;\n private canvasApp?: any;\n private canvasElement?: any;\n playerIdSignal = signal<string | null>(null);\n spriteComponentsBehind = signal<any[]>([]);\n spriteComponentsInFront = signal<any[]>([]);\n /** ID of the sprite that the camera should follow. null means follow the current player */\n cameraFollowTargetId = signal<string | null>(null);\n /** Trigger for map shake animation */\n mapShakeTrigger = trigger();\n\n controlsReady = signal(undefined); \n gamePause = signal(false);\n\n private predictionEnabled = false;\n private prediction?: PredictionController<Direction>;\n private readonly SERVER_CORRECTION_THRESHOLD = 30;\n private inputFrameCounter = 0;\n private pendingPredictionFrames: number[] = [];\n private lastClientPhysicsStepAt = 0;\n private frameOffset = 0;\n // Ping/Pong for RTT measurement\n private rtt: number = 0; // Round-trip time in ms\n private pingInterval: any = null;\n private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds\n private lastInputTime = 0;\n private readonly MOVE_PATH_RESEND_INTERVAL_MS = 120;\n private readonly MAX_MOVE_TRAJECTORY_POINTS = 240;\n private lastMovePathSentAt = 0;\n private lastMovePathSentFrame = 0;\n // Track map loading state for onAfterLoading hook using RxJS\n private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);\n private playerIdReceived$ = new BehaviorSubject<boolean>(false);\n private playersReceived$ = new BehaviorSubject<boolean>(false);\n private eventsReceived$ = new BehaviorSubject<boolean>(false);\n private onAfterLoadingSubscription?: any;\n private sceneResetQueued = false;\n \n // Store subscriptions and event listeners for cleanup\n private tickSubscriptions: any[] = [];\n private resizeHandler?: () => void;\n private notificationManager: NotificationManager = new NotificationManager();\n\n constructor(public context) {\n this.webSocket = inject(WebSocketToken);\n this.guiService = inject(RpgGui);\n this.loadMapService = inject(LoadMapToken);\n this.hooks = inject<Hooks>(ModulesToken);\n this.globalConfig = inject(GlobalConfigToken)\n\n if (!this.globalConfig) {\n this.globalConfig = {} as T\n }\n if (!(this.globalConfig as any).box) {\n (this.globalConfig as any).box = {\n styles: {\n backgroundColor: \"#1a1a2e\",\n backgroundOpacity: 0.9\n },\n sounds: {}\n }\n }\n\n this.addComponentAnimation({\n id: \"animation\",\n component: PrebuiltComponentAnimations.Animation\n })\n\n this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;\n this.initializePredictionController();\n }\n\n /**\n * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context\n * \n * This method registers a KeyboardControls instance from CanvasEngine into the DI container,\n * making it available for injection throughout the application. The particularity is that\n * this method is automatically called when a sprite is displayed on the map, allowing the\n * controls to be automatically associated with the active sprite.\n * \n * ## Design\n * \n * - The instance is stored in the DI context under the `KeyboardControls` token\n * - It's automatically assigned when a sprite component mounts (in `character.ce`)\n * - The controls instance comes from the CanvasEngine component's directives\n * - Once registered, it can be retrieved using `inject(KeyboardControls)` from anywhere\n * \n * @param controlInstance - The CanvasEngine KeyboardControls instance to register\n * \n * @example\n * ```ts\n * // The method is automatically called when a sprite is displayed:\n * // client.setKeyboardControls(element.directives.controls)\n * \n * // Later, retrieve and use the controls instance:\n * import { Input, inject, KeyboardControls } from '@rpgjs/client'\n * \n * const controls = inject(KeyboardControls)\n * const control = controls.getControl(Input.Enter)\n * \n * if (control) {\n * console.log(control.actionName) // 'action'\n * }\n * ```\n */\n setKeyboardControls(controlInstance: any) {\n const currentValues = this.context.values['inject:' + 'KeyboardControls']\n this.context.values['inject:' + 'KeyboardControls'] = {\n ...currentValues,\n values: new Map([['__default__', controlInstance]])\n }\n this.controlsReady.set(undefined);\n }\n\n async start() {\n this.sceneMap = new RpgClientMap()\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic();\n this.selector = document.body.querySelector(\"#rpg\") as HTMLElement;\n\n const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;\n const { app, canvasElement } = await bootstrapCanvas(\n this.selector,\n Canvas,\n bootstrapOptions\n );\n this.canvasApp = app;\n this.canvasElement = canvasElement;\n this.renderer = app.renderer as PIXI.Renderer;\n this.tick = canvasElement?.propObservables?.context['tick'].observable\n\n const inputCheckSubscription = this.tick.subscribe(() => {\n if (Date.now() - this.lastInputTime > 100) {\n const player = this.getCurrentPlayer();\n if (!player) return;\n (this.sceneMap as any).stopMovement(player);\n }\n });\n this.tickSubscriptions.push(inputCheckSubscription);\n\n\n this.hooks.callHooks(\"client-spritesheets-load\", this).subscribe();\n this.hooks.callHooks(\"client-spritesheetResolver-load\", this).subscribe();\n this.hooks.callHooks(\"client-sounds-load\", this).subscribe();\n this.hooks.callHooks(\"client-soundResolver-load\", this).subscribe();\n \n RpgSound.init(this);\n RpgResource.init(this);\n this.hooks.callHooks(\"client-gui-load\", this).subscribe();\n this.hooks.callHooks(\"client-particles-load\", this).subscribe();\n this.hooks.callHooks(\"client-componentAnimations-load\", this).subscribe();\n this.hooks.callHooks(\"client-sprite-load\", this).subscribe();\n\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onStart\", this));\n\n // wondow is resize\n this.resizeHandler = () => {\n this.hooks.callHooks(\"client-engine-onWindowResize\", this).subscribe();\n };\n window.addEventListener('resize', this.resizeHandler);\n\n const tickSubscription = this.tick.subscribe((tick) => {\n this.stepClientPhysicsTick();\n this.flushPendingPredictedStates();\n this.flushPendingMovePath();\n this.hooks.callHooks(\"client-engine-onStep\", this, tick).subscribe();\n\n // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)\n if (tick % 60 === 0) {\n const now = Date.now();\n this.prediction?.cleanup(now);\n this.prediction?.tryApplyPendingSnapshot();\n }\n });\n this.tickSubscriptions.push(tickSubscription);\n\n await this.webSocket.connection(() => {\n const saveClient = inject(SaveClientService);\n saveClient.initialize(this.webSocket);\n this.initListeners()\n this.guiService._initialize()\n this.startPingPong();\n });\n }\n\n private prepareSyncPayload(data: any): any {\n const payload = { ...(data ?? {}) };\n delete payload.ack;\n delete payload.timestamp;\n\n const myId = this.playerIdSignal();\n const players = payload.players;\n const shouldMaskLocalPosition =\n this.predictionEnabled && !!this.prediction?.hasPendingInputs();\n if (shouldMaskLocalPosition && myId && players && players[myId]) {\n const localPatch = { ...players[myId] };\n delete localPatch.x;\n delete localPatch.y;\n delete localPatch.direction;\n delete localPatch._frames;\n payload.players = {\n ...players,\n [myId]: localPatch,\n };\n }\n\n return payload;\n }\n\n private normalizeAckWithSyncState(\n ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },\n syncData: any,\n ): { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction } {\n const myId = this.playerIdSignal();\n if (!myId) {\n return ack;\n }\n\n const localPatch = syncData?.players?.[myId];\n if (typeof localPatch?.x !== \"number\" || typeof localPatch?.y !== \"number\") {\n return ack;\n }\n\n return {\n ...ack,\n x: localPatch.x,\n y: localPatch.y,\n direction: localPatch.direction ?? ack.direction,\n };\n }\n\n private initListeners() {\n this.webSocket.on(\"sync\", (data) => {\n if (data.pId) {\n this.playerIdSignal.set(data.pId);\n // Signal that player ID was received\n this.playerIdReceived$.next(true);\n }\n\n if (this.sceneResetQueued) {\n this.sceneMap.reset();\n this.sceneMap.loadPhysic();\n this.sceneResetQueued = false;\n }\n\n // Apply client-side prediction filtering and server reconciliation\n this.hooks.callHooks(\"client-sceneMap-onChanges\", this.sceneMap, { partial: data }).subscribe();\n\n const ack = data?.ack;\n const normalizedAck =\n ack && typeof ack.frame === \"number\"\n ? this.normalizeAckWithSyncState(ack, data)\n : undefined;\n const payload = this.prepareSyncPayload(data);\n load(this.sceneMap, payload, true);\n\n if (normalizedAck) {\n this.applyServerAck(normalizedAck);\n }\n\n for (const playerId in payload.players ?? {}) {\n const player = payload.players[playerId]\n if (!player._param) continue\n for (const param in player._param) {\n this.sceneMap.players()[playerId]._param()[param] = player._param[param]\n }\n }\n \n // Check if players and events are present in sync data\n const players = payload.players || this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = payload.events || this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n });\n\n // Handle pong responses for RTT measurement\n this.webSocket.on(\"pong\", (data: { serverTick: number; clientFrame: number; clientTime: number }) => {\n const now = Date.now();\n this.rtt = now - data.clientTime;\n\n // Calculate frame offset: how many ticks ahead the server is compared to our frame counter\n // This helps us estimate which server tick corresponds to each client input frame\n const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT\n const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;\n\n // Update frame offset (only if we have inputs to calibrate with)\n if (this.inputFrameCounter > 0) {\n this.frameOffset = estimatedServerTickNow - data.clientFrame;\n }\n\n console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);\n });\n\n this.webSocket.on(\"changeMap\", (data) => {\n this.sceneResetQueued = true;\n // Reset camera follow to default (follow current player) when changing maps\n this.cameraFollowTargetId.set(null);\n const transferToken = typeof data?.transferToken === \"string\" ? data.transferToken : undefined;\n this.loadScene(data.mapId, transferToken);\n });\n\n this.webSocket.on(\"showComponentAnimation\", (data) => {\n const { params, object, position, id } = data;\n if (!object && position === undefined) {\n throw new Error(\"Please provide an object or x and y coordinates\");\n }\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n this.getComponentAnimation(id).displayEffect(params, player || position)\n });\n\n this.webSocket.on(\"notification\", (data) => {\n this.notificationManager.add(data);\n });\n\n this.webSocket.on(\"setAnimation\", (data) => { \n const { animationName, nbTimes, object, graphic } = data;\n const player = this.sceneMap.getObjectById(object);\n if (graphic !== undefined) {\n player.setAnimation(animationName, graphic, nbTimes);\n } else {\n player.setAnimation(animationName, nbTimes);\n }\n })\n\n this.webSocket.on(\"playSound\", (data) => {\n const { soundId, volume, loop } = data;\n this.playSound(soundId, { volume, loop });\n });\n\n this.webSocket.on(\"stopSound\", (data) => {\n const { soundId } = data;\n this.stopSound(soundId);\n });\n\n this.webSocket.on(\"stopAllSounds\", () => {\n this.stopAllSounds();\n });\n\n this.webSocket.on(\"cameraFollow\", (data) => {\n const { targetId, smoothMove } = data;\n this.setCameraFollow(targetId, smoothMove);\n });\n\n this.webSocket.on(\"flash\", (data) => {\n const { object, type, duration, cycles, alpha, tint } = data;\n const sprite = object ? this.sceneMap.getObjectById(object) : undefined;\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash({ type, duration, cycles, alpha, tint });\n }\n });\n\n this.webSocket.on(\"shakeMap\", (data) => {\n const { intensity, duration, frequency, direction } = data || {};\n (this.mapShakeTrigger as any).start({\n intensity,\n duration,\n frequency,\n direction\n });\n });\n\n this.webSocket.on(\"weatherState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n if (raw === null) {\n this.sceneMap.weatherState.set(null);\n return;\n }\n\n const validEffects = [\"rain\", \"snow\", \"fog\", \"cloud\"];\n if (!raw || !validEffects.includes((raw as any).effect)) {\n return;\n }\n\n this.sceneMap.weatherState.set({\n effect: (raw as any).effect,\n preset: (raw as any).preset,\n params: (raw as any).params,\n transitionMs: (raw as any).transitionMs,\n durationMs: (raw as any).durationMs,\n startedAt: (raw as any).startedAt,\n seed: (raw as any).seed,\n });\n });\n\n this.webSocket.on('open', () => {\n this.hooks.callHooks(\"client-engine-onConnected\", this, this.socket).subscribe();\n // Start ping/pong for synchronization\n this.startPingPong();\n })\n\n this.webSocket.on('close', () => {\n this.hooks.callHooks(\"client-engine-onDisconnected\", this, this.socket).subscribe();\n // Stop ping/pong when disconnected\n this.stopPingPong();\n })\n\n this.webSocket.on('error', (error) => {\n this.hooks.callHooks(\"client-engine-onConnectError\", this, error, this.socket).subscribe();\n })\n }\n\n /**\n * Start periodic ping/pong for client-server synchronization\n * \n * Sends ping requests to the server to measure round-trip time (RTT) and\n * calculate the frame offset between client and server ticks.\n * \n * ## Design\n * \n * - Sends ping every 5 seconds\n * - Measures RTT for latency compensation\n * - Calculates frame offset to map client frames to server ticks\n * - Used for accurate server reconciliation\n * \n * @example\n * ```ts\n * // Called automatically when connection opens\n * this.startPingPong();\n * ```\n */\n private startPingPong(): void {\n // Stop existing interval if any\n this.stopPingPong();\n\n // Send initial ping immediately\n this.sendPing();\n\n // Set up periodic pings\n this.pingInterval = setInterval(() => {\n this.sendPing();\n }, this.PING_INTERVAL_MS);\n }\n\n /**\n * Stop periodic ping/pong\n * \n * Stops the ping interval when disconnecting or changing maps.\n * \n * @example\n * ```ts\n * // Called automatically when connection closes\n * this.stopPingPong();\n * ```\n */\n private stopPingPong(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Send a ping request to the server\n * \n * Sends current client time and frame counter to the server,\n * which will respond with its server tick for synchronization.\n * \n * @example\n * ```ts\n * // Send a ping to measure RTT\n * this.sendPing();\n * ```\n */\n private sendPing(): void {\n const clientTime = Date.now();\n const clientFrame = this.getPhysicsTick();\n\n this.webSocket.emit('ping', {\n clientTime,\n clientFrame\n });\n }\n\n private async loadScene(mapId: string, transferToken?: string) {\n await lastValueFrom(this.hooks.callHooks(\"client-sceneMap-onBeforeLoading\", this.sceneMap));\n\n // Clear client prediction states when changing maps\n this.clearClientPredictionStates();\n\n // Reset all conditions for new map loading\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n\n // Unsubscribe previous subscription if exists\n if (this.onAfterLoadingSubscription) {\n this.onAfterLoadingSubscription.unsubscribe();\n }\n\n // Setup RxJS observable to wait for all conditions\n this.setupOnAfterLoadingObserver();\n\n this.webSocket.updateProperties({\n room: mapId,\n query: transferToken ? { transferToken } : undefined,\n })\n await this.webSocket.reconnect(() => {\n const saveClient = inject(SaveClientService);\n saveClient.initialize(this.webSocket);\n this.initListeners()\n this.guiService._initialize()\n })\n const res = await this.loadMapService.load(mapId)\n this.sceneMap.data.set(res)\n \n // Check if playerId is already present\n if (this.playerIdSignal()) {\n this.playerIdReceived$.next(true);\n }\n \n // Check if players and events are already present in sceneMap\n const players = this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n \n // Signal that map loading is completed (this should be last to ensure other checks are done)\n this.mapLoadCompleted$.next(true);\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic()\n }\n\n addSpriteSheet<T = any>(spritesheetClass: any, id?: string): any {\n this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);\n return spritesheetClass as any;\n }\n\n /**\n * Set a resolver function for spritesheets\n * \n * The resolver is called when a spritesheet is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSpritesheetResolver((id) => {\n * if (id === 'dynamic-sprite') {\n * return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSpritesheetResolver(async (id) => {\n * const response = await fetch(`/api/spritesheets/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {\n this.spritesheetResolver = resolver;\n }\n\n /**\n * Get a spritesheet by ID, using resolver if not found in cache\n * \n * This method first checks if the spritesheet exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the spritesheet.\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param id - The spritesheet ID to retrieve\n * @returns The spritesheet if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const spritesheet = engine.getSpriteSheet('my-sprite');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');\n * ```\n */\n getSpriteSheet(id: string): any | Promise<any> {\n // Check cache first\n if (this.spritesheets.has(id)) {\n return this.spritesheets.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.spritesheetResolver) {\n const result = this.spritesheetResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((spritesheet) => {\n if (spritesheet) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, spritesheet);\n }\n return spritesheet;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Add a sound to the engine\n * \n * Adds a sound to the engine's sound cache. The sound can be:\n * - A simple object with `id` and `src` properties\n * - A Howler instance\n * - An object with a `play()` method\n * \n * If the sound has a `src` property, a Howler instance will be created automatically.\n * \n * @param sound - The sound object or Howler instance\n * @param id - Optional sound ID (if not provided, uses sound.id)\n * @returns The added sound\n * \n * @example\n * ```ts\n * // Simple sound object\n * engine.addSound({ id: 'click', src: 'click.mp3' });\n * \n * // With explicit ID\n * engine.addSound({ src: 'music.mp3' }, 'background-music');\n * ```\n */\n addSound(sound: any, id?: string): any {\n const soundId = id || sound.id;\n \n if (!soundId) {\n console.warn('Sound added without an ID. It will not be retrievable.');\n return sound;\n }\n\n // If sound has a src property, create a Howler instance\n if (sound.src && typeof sound.src === 'string') {\n const howlOptions: any = {\n src: [sound.src],\n loop: sound.loop || false,\n volume: sound.volume !== undefined ? sound.volume : 1.0,\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n this.sounds.set(soundId, howl);\n return howl;\n }\n\n // If sound already has a play method (Howler instance or custom), use it directly\n if (sound && typeof sound.play === 'function') {\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n // Otherwise, store as-is\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n /**\n * Set a resolver function for sounds\n * \n * The resolver is called when a sound is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved sound is automatically cached for future use.\n * \n * @param resolver - Function that takes a sound ID and returns a sound or Promise of sound\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSoundResolver((id) => {\n * if (id === 'dynamic-sound') {\n * return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSoundResolver(async (id) => {\n * const response = await fetch(`/api/sounds/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSoundResolver(resolver: (id: string) => any | Promise<any>): void {\n this.soundResolver = resolver;\n }\n\n /**\n * Get a sound by ID, using resolver if not found in cache\n * \n * This method first checks if the sound exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the sound.\n * The resolved sound is automatically cached for future use.\n * \n * @param id - The sound ID to retrieve\n * @returns The sound if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const sound = engine.getSound('my-sound');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const sound = await engine.getSound('dynamic-sound');\n * ```\n */\n getSound(id: string): any | Promise<any> {\n // Check cache first\n if (this.sounds.has(id)) {\n return this.sounds.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.soundResolver) {\n const result = this.soundResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((sound) => {\n if (sound) {\n // Cache the resolved sound\n this.sounds.set(id, sound);\n }\n return sound;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved sound\n this.sounds.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Play a sound by its ID\n * \n * This method retrieves a sound from the cache or resolver and plays it.\n * If the sound is not found, it will attempt to resolve it using the soundResolver.\n * Uses Howler.js for audio playback instead of native Audio elements.\n * \n * @param soundId - The sound ID to play\n * @param options - Optional sound configuration\n * @param options.volume - Volume level (0.0 to 1.0, overrides sound default)\n * @param options.loop - Whether the sound should loop (overrides sound default)\n * \n * @example\n * ```ts\n * // Play a sound synchronously\n * engine.playSound('item-pickup');\n * \n * // Play a sound with volume and loop\n * engine.playSound('background-music', { volume: 0.5, loop: true });\n * \n * // Play a sound asynchronously (when resolver returns Promise)\n * await engine.playSound('dynamic-sound', { volume: 0.8 });\n * ```\n */\n async playSound(soundId: string, options?: { volume?: number; loop?: boolean }): Promise<void> {\n const sound = await this.getSound(soundId);\n if (sound && sound.play) {\n // Sound is already a Howler instance or has a play method\n const howlSoundId = sound._sounds?.[0]?._id;\n \n // Apply volume if provided\n if (options?.volume !== undefined) {\n if (howlSoundId !== undefined) {\n sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);\n } else {\n sound.volume(Math.max(0, Math.min(1, options.volume)));\n }\n }\n \n // Apply loop if provided\n if (options?.loop !== undefined) {\n if (howlSoundId !== undefined) {\n sound.loop(options.loop, howlSoundId);\n } else {\n sound.loop(options.loop);\n }\n }\n \n if (howlSoundId !== undefined) {\n sound.play(howlSoundId);\n } else {\n sound.play();\n }\n } else if (sound && sound.src) {\n // If sound is just a source URL, create a Howler instance and cache it\n const howlOptions: any = {\n src: [sound.src],\n loop: options?.loop !== undefined ? options.loop : (sound.loop || false),\n volume: options?.volume !== undefined ? Math.max(0, Math.min(1, options.volume)) : (sound.volume !== undefined ? sound.volume : 1.0),\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n \n // Cache the Howler instance for future use\n this.sounds.set(soundId, howl);\n \n // Play the sound\n howl.play();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be played`);\n }\n }\n\n /**\n * Stop a sound that is currently playing\n * \n * This method stops a sound that was previously started with `playSound()`.\n * \n * @param soundId - The sound ID to stop\n * \n * @example\n * ```ts\n * // Start a looping sound\n * engine.playSound('background-music', { loop: true });\n * \n * // Later, stop it\n * engine.stopSound('background-music');\n * ```\n */\n stopSound(soundId: string): void {\n const sound = this.sounds.get(soundId);\n if (sound && sound.stop) {\n sound.stop();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be stopped`);\n }\n }\n\n /**\n * Stop all currently playing sounds\n * \n * This method stops all sounds that are currently playing.\n * Useful when changing maps to prevent sound overlap.\n * \n * @example\n * ```ts\n * // Stop all sounds\n * engine.stopAllSounds();\n * ```\n */\n stopAllSounds(): void {\n this.sounds.forEach((sound) => {\n if (sound && sound.stop) {\n sound.stop();\n }\n });\n }\n\n /**\n * Set the camera to follow a specific sprite\n * \n * This method changes which sprite the camera viewport should follow.\n * The camera will smoothly animate to the target sprite if smoothMove options are provided.\n * \n * ## Design\n * \n * The camera follow target is stored in a signal that is read by sprite components.\n * Each sprite checks if it should be followed by comparing its ID with the target ID.\n * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's\n * viewport system.\n * \n * @param targetId - The ID of the sprite to follow. Set to null to follow the current player\n * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease\n * @param smoothMove.time - Duration of the animation in milliseconds (optional)\n * @param smoothMove.ease - Easing function name from https://easings.net (optional)\n * \n * @example\n * ```ts\n * // Follow another player with default smooth animation\n * engine.setCameraFollow(otherPlayerId, true);\n * \n * // Follow an event with custom smooth animation\n * engine.setCameraFollow(eventId, {\n * time: 1000,\n * ease: \"easeInOutQuad\"\n * });\n * \n * // Follow without animation (instant)\n * engine.setCameraFollow(targetId, false);\n * \n * // Return to following current player\n * engine.setCameraFollow(null);\n * ```\n */\n setCameraFollow(\n targetId: string | null,\n smoothMove?: boolean | { time?: number; ease?: string }\n ): void {\n // Store smoothMove options for potential future use with viewport animation\n // For now, we just set the target ID and let CanvasEngine handle the viewport follow\n // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it\n this.cameraFollowTargetId.set(targetId);\n \n // If smoothMove is an object, we could store it for viewport configuration\n // This would require integration with CanvasEngine's viewport animation system\n if (typeof smoothMove === \"object\" && smoothMove !== null) {\n // Future: Apply smoothMove.time and smoothMove.ease to viewport animation\n // For now, CanvasEngine handles viewport following automatically\n }\n }\n\n addParticle(particle: any) {\n this.particleSettings.emitters.push(particle)\n return particle;\n }\n\n /**\n * Add a component to render behind sprites\n * Components added with this method will be displayed with a lower z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `ShadowComponent`\n * 2. Configuration object: `{ component: LightHalo, props: {...} }`\n * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add behind sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a shadow component behind all sprites\n * engine.addSpriteComponentBehind(ShadowComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentBehind({ \n * component: LightHalo, \n * props: { radius: 30 } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentBehind({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentBehind(component: any) {\n this.spriteComponentsBehind.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component to render in front of sprites\n * Components added with this method will be displayed with a higher z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `HealthBarComponent`\n * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`\n * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add in front of sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a health bar component in front of all sprites\n * engine.addSpriteComponentInFront(HealthBarComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentInFront({ \n * component: StatusIndicator, \n * props: { type: 'poison' } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentInFront({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {\n this.spriteComponentsInFront.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component animation to the engine\n * \n * Component animations are temporary visual effects that can be displayed\n * on sprites or objects, such as hit indicators, spell effects, or status animations.\n * \n * @param componentAnimation - The component animation configuration\n * @param componentAnimation.id - Unique identifier for the animation\n * @param componentAnimation.component - The component function to render\n * @returns The added component animation configuration\n * \n * @example\n * ```ts\n * // Add a hit animation component\n * engine.addComponentAnimation({\n * id: 'hit',\n * component: HitComponent\n * });\n * \n * // Add an explosion effect component\n * engine.addComponentAnimation({\n * id: 'explosion',\n * component: ExplosionComponent\n * });\n * ```\n */\n addComponentAnimation(componentAnimation: {\n component: any,\n id: string\n }) {\n const instance = new AnimationManager()\n this.componentAnimations.push({\n id: componentAnimation.id,\n component: componentAnimation.component,\n instance: instance,\n current: instance.current\n })\n return componentAnimation;\n }\n\n /**\n * Get a component animation by its ID\n * \n * Retrieves the EffectManager instance for a specific component animation,\n * which can be used to display the animation on sprites or objects.\n * \n * @param id - The unique identifier of the component animation\n * @returns The EffectManager instance for the animation\n * @throws Error if the component animation is not found\n * \n * @example\n * ```ts\n * // Get the hit animation and display it\n * const hitAnimation = engine.getComponentAnimation('hit');\n * hitAnimation.displayEffect({ text: \"Critical!\" }, player);\n * ```\n */\n getComponentAnimation(id: string): AnimationManager {\n const componentAnimation = this.componentAnimations.find((componentAnimation) => componentAnimation.id === id)\n if (!componentAnimation) {\n throw new Error(`Component animation with id ${id} not found`)\n }\n return componentAnimation.instance\n }\n\n /**\n * Start a transition\n * \n * Convenience method to display a transition by its ID using the GUI system.\n * \n * @param id - The unique identifier of the transition to start\n * @param props - Props to pass to the transition component\n * \n * @example\n * ```ts\n * // Start a fade transition\n * engine.startTransition('fade', { duration: 1000, color: 'black' });\n * \n * // Start with onFinish callback\n * engine.startTransition('fade', {\n * duration: 1000,\n * onFinish: () => console.log('Fade complete')\n * });\n * ```\n */\n startTransition(id: string, props: any = {}) {\n if (!this.guiService.exists(id)) {\n throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);\n }\n this.guiService.display(id, props);\n }\n\n async processInput({ input }: { input: Direction }) {\n const timestamp = Date.now();\n let frame: number;\n let tick: number;\n if (this.predictionEnabled && this.prediction) {\n const meta = this.prediction.recordInput(input, timestamp);\n frame = meta.frame;\n tick = meta.tick;\n } else {\n frame = ++this.inputFrameCounter;\n tick = this.getPhysicsTick();\n }\n this.inputFrameCounter = frame;\n this.hooks.callHooks(\"client-engine-onInput\", this, { input, playerId: this.playerId }).subscribe();\n\n const currentPlayer = this.sceneMap.getCurrentPlayer();\n const bodyReady = this.ensureCurrentPlayerBody();\n if (currentPlayer && bodyReady) {\n currentPlayer.changeDirection(input);\n (this.sceneMap as any).moveBody(currentPlayer, input);\n if (this.predictionEnabled && this.prediction) {\n this.pendingPredictionFrames.push(frame);\n if (this.pendingPredictionFrames.length > 240) {\n this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);\n }\n }\n }\n\n this.emitMovePacket(input, frame, tick, timestamp, true);\n this.lastInputTime = Date.now();\n }\n\n processAction({ action }: { action: number }) {\n if (this.stopProcessingInput) return;\n this.hooks.callHooks(\"client-engine-onInput\", this, { input: 'action', playerId: this.playerId }).subscribe();\n this.webSocket.emit('action', { action })\n }\n\n get PIXI() {\n return PIXI\n }\n\n get socket() {\n return this.webSocket\n }\n\n get playerId() {\n return this.playerIdSignal()\n }\n\n get scene() {\n return this.sceneMap\n }\n\n private getPhysicsTick(): number {\n return this.sceneMap?.getTick?.() ?? 0;\n }\n\n private ensureCurrentPlayerBody(): boolean {\n const player = this.sceneMap?.getCurrentPlayer();\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return false;\n }\n if (!player.id) {\n player.id = myId;\n }\n if (this.sceneMap.getBody(myId)) {\n return true;\n }\n try {\n this.sceneMap.loadPhysic();\n } catch (error) {\n console.error(\"[RPGJS] Unable to initialize client physics before input:\", error);\n return false;\n }\n return !!this.sceneMap.getBody(myId);\n }\n\n private stepClientPhysicsTick(): void {\n if (!this.predictionEnabled || !this.sceneMap) {\n return;\n }\n const now = Date.now();\n if (this.lastClientPhysicsStepAt === 0) {\n this.lastClientPhysicsStepAt = now;\n }\n const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));\n this.lastClientPhysicsStepAt = now;\n this.sceneMap.stepClientPhysics(deltaMs);\n }\n\n private flushPendingPredictedStates(): void {\n if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {\n return;\n }\n const state = this.getLocalPlayerState();\n while (this.pendingPredictionFrames.length > 0) {\n const frame = this.pendingPredictionFrames.shift();\n if (typeof frame === \"number\") {\n this.prediction.attachPredictedState(frame, state);\n }\n }\n }\n\n private buildPendingMoveTrajectory(): MovementTrajectoryPoint[] {\n if (!this.predictionEnabled || !this.prediction) {\n return [];\n }\n const pendingInputs = this.prediction.getPendingInputs();\n const trajectory: MovementTrajectoryPoint[] = [];\n for (const entry of pendingInputs) {\n const state = entry.state;\n if (!state) continue;\n if (typeof state.x !== \"number\" || typeof state.y !== \"number\") continue;\n trajectory.push({\n frame: entry.frame,\n tick: entry.tick,\n timestamp: entry.timestamp,\n input: entry.direction,\n x: state.x,\n y: state.y,\n direction: state.direction ?? entry.direction,\n });\n }\n if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {\n return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);\n }\n return trajectory;\n }\n\n private emitMovePacket(\n input: Direction,\n frame: number,\n tick: number,\n timestamp: number,\n force = false,\n ): void {\n const trajectory = this.buildPendingMoveTrajectory();\n const latestTrajectoryFrame =\n trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;\n const shouldThrottle =\n !force &&\n latestTrajectoryFrame <= this.lastMovePathSentFrame &&\n timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;\n if (shouldThrottle) {\n return;\n }\n\n this.webSocket.emit(\"move\", {\n input,\n timestamp,\n frame,\n tick,\n trajectory,\n });\n this.lastMovePathSentAt = timestamp;\n this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);\n }\n\n private flushPendingMovePath(): void {\n if (!this.predictionEnabled || !this.prediction) {\n return;\n }\n const pendingInputs = this.prediction.getPendingInputs();\n if (pendingInputs.length === 0) {\n return;\n }\n const latest = pendingInputs[pendingInputs.length - 1];\n if (!latest) {\n return;\n }\n const now = Date.now();\n if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {\n return;\n }\n this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);\n }\n\n private getLocalPlayerState(): PredictionState<Direction> {\n const currentPlayer = this.sceneMap?.getCurrentPlayer();\n if (!currentPlayer) {\n return { x: 0, y: 0, direction: Direction.Down };\n }\n const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, \"top-left\");\n const x = topLeft?.x ?? currentPlayer.x();\n const y = topLeft?.y ?? currentPlayer.y();\n const direction = currentPlayer.direction();\n return { x, y, direction };\n }\n\n private applyAuthoritativeState(state: PredictionState<Direction>): void {\n const player = this.sceneMap?.getCurrentPlayer();\n if (!player) return;\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(player.id, state.x, state.y, \"top-left\");\n }\n player.x.set(Math.round(state.x));\n player.y.set(Math.round(state.y));\n if (state.direction) {\n player.changeDirection(state.direction);\n }\n }\n\n private initializePredictionController(): void {\n if (!this.predictionEnabled) {\n this.prediction = undefined;\n this.sceneMap?.configureClientPrediction?.(false);\n return;\n }\n const configuredTtl = (this.globalConfig as any)?.prediction?.historyTtlMs;\n const historyTtlMs = typeof configuredTtl === \"number\" ? configuredTtl : 10000;\n const configuredMaxEntries = (this.globalConfig as any)?.prediction?.maxHistoryEntries;\n const maxHistoryEntries =\n typeof configuredMaxEntries === \"number\"\n ? configuredMaxEntries\n : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);\n this.sceneMap?.configureClientPrediction?.(true);\n this.prediction = new PredictionController<Direction>({\n correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,\n historyTtlMs,\n maxHistoryEntries,\n getPhysicsTick: () => this.getPhysicsTick(),\n getCurrentState: () => this.getLocalPlayerState(),\n setAuthoritativeState: (state) => this.applyAuthoritativeState(state),\n });\n }\n\n getCurrentPlayer() {\n return this.sceneMap.getCurrentPlayer()\n }\n\n emitSceneMapHook(hookName: string, ...args: any[]): void {\n this.hooks.callHooks(`client-sceneMap-${hookName}`, ...args).subscribe();\n }\n\n /**\n * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook\n * \n * This method uses RxJS `combineLatest` to wait for all conditions to be met,\n * regardless of the order in which they arrive:\n * 1. The map loading is completed (loadMapService.load is finished)\n * 2. We received a player ID (pId)\n * 3. Players array has at least one element\n * 4. Events property is present in the sync data\n * \n * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.\n * \n * ## Design\n * \n * Uses BehaviorSubjects to track each condition state, allowing events to arrive\n * in any order. The `combineLatest` operator waits until all observables emit `true`,\n * then `take(1)` ensures the hook is called only once, and `switchMap` handles\n * the hook execution.\n * \n * @example\n * ```ts\n * // Called automatically in loadScene to setup the observer\n * this.setupOnAfterLoadingObserver();\n * ```\n */\n private setupOnAfterLoadingObserver(): void {\n this.onAfterLoadingSubscription = combineLatest([\n this.mapLoadCompleted$.pipe(filter(completed => completed === true)),\n this.playerIdReceived$.pipe(filter(received => received === true)),\n this.playersReceived$.pipe(filter(received => received === true)),\n this.eventsReceived$.pipe(filter(received => received === true))\n ]).pipe(\n take(1), // Only execute once when all conditions are met\n switchMap(() => {\n // Call the hook and return the observable\n return this.hooks.callHooks(\"client-sceneMap-onAfterLoading\", this.sceneMap);\n })\n ).subscribe();\n }\n\n /**\n * Clear client prediction states for cleanup\n * \n * Removes old prediction states and input history to prevent memory leaks.\n * Should be called when changing maps or disconnecting.\n * \n * @example\n * ```ts\n * // Clear prediction states when changing maps\n * engine.clearClientPredictionStates();\n * ```\n */\n clearClientPredictionStates() {\n this.initializePredictionController();\n this.frameOffset = 0;\n this.inputFrameCounter = 0;\n this.pendingPredictionFrames = [];\n this.lastClientPhysicsStepAt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n }\n\n /**\n * Trigger a flash animation on a sprite\n * \n * This method allows you to trigger a flash effect on any sprite from client-side code.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash is applied directly to the sprite object using its flash trigger.\n * This is useful for client-side visual feedback, UI interactions, or local effects\n * that don't need to be synchronized with the server.\n * \n * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player\n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Flash the current player with default settings\n * engine.flash();\n * \n * // Flash a specific sprite with red tint\n * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint for dramatic effect\n * engine.flash(undefined, { \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash on current player\n * engine.flash(undefined, { \n * type: 'tint', \n * tint: 'red', \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(\n spriteId?: string,\n options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }\n ): void {\n const targetId = spriteId || this.playerId;\n if (!targetId) return;\n\n const sprite = this.sceneMap.getObjectById(targetId);\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash(options);\n }\n }\n\n private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {\n if (this.predictionEnabled && this.prediction) {\n const result = this.prediction.applyServerAck({\n frame: ack.frame,\n serverTick: ack.serverTick,\n state:\n typeof ack.x === \"number\" && typeof ack.y === \"number\"\n ? { x: ack.x, y: ack.y, direction: ack.direction }\n : undefined,\n });\n if (result.state && result.needsReconciliation) {\n this.reconcilePrediction(result.state, result.pendingInputs);\n }\n return;\n }\n\n if (typeof ack.x !== \"number\" || typeof ack.y !== \"number\") {\n return;\n }\n const player = this.getCurrentPlayer();\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return;\n }\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(myId, ack.x, ack.y, \"top-left\");\n }\n player.x.set(Math.round(ack.x));\n player.y.set(Math.round(ack.y));\n if (ack.direction) {\n player.changeDirection(ack.direction);\n }\n }\n\n private reconcilePrediction(\n authoritativeState: PredictionState<Direction>,\n pendingInputs: PredictionHistoryEntry<Direction>[],\n ): void {\n const player = this.getCurrentPlayer();\n if (!player) {\n return;\n }\n\n (this.sceneMap as any).stopMovement(player);\n this.applyAuthoritativeState(authoritativeState);\n\n if (!pendingInputs.length) {\n return;\n }\n\n // Keep replay bounded while still tolerating high-latency links.\n const replayInputs = pendingInputs.slice(-600);\n for (const entry of replayInputs) {\n if (!entry?.direction) continue;\n (this.sceneMap as any).moveBody(player, entry.direction);\n this.sceneMap.stepPredictionTick();\n this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());\n }\n }\n\n /**\n * Replay unacknowledged inputs from a given frame to resimulate client prediction\n * after applying server authority at a certain frame.\n * \n * @param startFrame - The last server-acknowledged frame\n * \n * @example\n * ```ts\n * // After applying a server correction at frame N\n * this.replayUnackedInputsFromFrame(N);\n * ```\n */\n private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {\n // Prediction controller handles replay internally. Kept for backwards compatibility.\n }\n\n /**\n * Clear all client resources and reset state\n * \n * This method should be called to clean up all client-side resources when\n * shutting down or resetting the client engine. It:\n * - Destroys the PIXI renderer\n * - Stops all sounds\n * - Cleans up subscriptions and event listeners\n * - Resets scene map\n * - Stops ping/pong interval\n * - Clears prediction states\n * \n * ## Design\n * \n * This method is used primarily in testing environments to ensure clean\n * state between tests. In production, the client engine typically persists\n * for the lifetime of the application.\n * \n * @example\n * ```ts\n * // In test cleanup\n * afterEach(() => {\n * clientEngine.clear();\n * });\n * ```\n */\n clear(): void {\n try {\n // First, unsubscribe from all tick subscriptions to stop rendering attempts\n for (const subscription of this.tickSubscriptions) {\n if (subscription && typeof subscription.unsubscribe === 'function') {\n subscription.unsubscribe();\n }\n }\n this.tickSubscriptions = [];\n\n // Stop ping/pong interval\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n\n // Clean up onAfterLoading subscription\n if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {\n this.onAfterLoadingSubscription.unsubscribe();\n this.onAfterLoadingSubscription = undefined;\n }\n\n // Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app\n // This prevents CanvasEngine from trying to render after PIXI is destroyed\n // CanvasEngine manages its own render loop which could try to access PIXI after destruction\n if (this.canvasElement) {\n try {\n // Try to stop or cleanup canvasElement if it has cleanup methods\n if (typeof (this.canvasElement as any).destroy === 'function') {\n (this.canvasElement as any).destroy();\n }\n // Clear the reference\n this.canvasElement = undefined;\n } catch (error) {\n // Ignore errors during canvasElement cleanup\n }\n }\n\n // Reset scene map if it exists (this should stop any ongoing animations/renders)\n if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {\n (this.sceneMap as any).reset(true);\n }\n\n // Stop all sounds\n this.stopAllSounds();\n\n // Remove resize event listener\n if (this.resizeHandler && typeof window !== 'undefined') {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = undefined;\n }\n\n // Destroy PIXI app and renderer if they exist\n // Destroy the app first, which will destroy the renderer\n // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)\n const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';\n \n if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {\n try {\n // Stop the ticker first to prevent any render calls during destruction\n if (this.canvasApp.ticker) {\n if (typeof this.canvasApp.ticker.stop === 'function') {\n this.canvasApp.ticker.stop();\n }\n // Also remove all listeners from ticker to prevent callbacks\n if (typeof this.canvasApp.ticker.removeAll === 'function') {\n this.canvasApp.ticker.removeAll();\n }\n }\n \n // Stop the renderer's ticker if it exists separately\n if (this.renderer && (this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n // Remove the canvas from DOM before destroying to prevent render attempts\n if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {\n this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);\n }\n \n // Destroy with minimal options to avoid issues\n // Don't pass options that might trigger additional cleanup that could fail\n this.canvasApp.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.canvasApp = undefined;\n // canvasApp.destroy() already destroyed the renderer, so just null it\n this.renderer = null as any;\n } else if (rendererStillExists) {\n // Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed\n try {\n // Stop the renderer's ticker if it has one\n if ((this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n this.renderer.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.renderer = null as any;\n }\n\n // Clean up prediction controller\n if (this.prediction) {\n // Prediction controller cleanup is handled internally when destroyed\n this.prediction = undefined;\n }\n\n // Reset signals\n this.playerIdSignal.set(null);\n this.cameraFollowTargetId.set(null);\n this.spriteComponentsBehind.set([]);\n this.spriteComponentsInFront.set([]);\n \n // Clear maps and arrays\n this.spritesheets.clear();\n this.sounds.clear();\n this.componentAnimations = [];\n this.particleSettings.emitters = [];\n\n // Reset state\n this.stopProcessingInput = false;\n this.lastInputTime = 0;\n this.inputFrameCounter = 0;\n this.frameOffset = 0;\n this.rtt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n\n // Reset behavior subjects\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n } catch (error) {\n console.warn('Error during client engine cleanup:', error);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RpgClientEngine.js","names":[],"sources":["../src/RpgClientEngine.ts"],"sourcesContent":["import Canvas from \"./components/scenes/canvas.ce\";\nimport { inject } from './core/inject'\nimport { signal, bootstrapCanvas, Howl, trigger } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"./services/AbstractSocket\";\nimport { LoadMapService, LoadMapToken } from \"./services/loadMap\";\nimport { RpgSound } from \"./Sound\";\nimport { RpgResource } from \"./Resource\";\nimport { Hooks, ModulesToken, Direction } from \"@rpgjs/common\";\nimport { load } from \"@signe/sync\";\nimport { RpgClientMap } from \"./Game/Map\"\nimport { RpgGui } from \"./Gui/Gui\";\nimport { AnimationManager } from \"./Game/AnimationManager\";\nimport { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from \"rxjs\";\nimport { GlobalConfigToken } from \"./module\";\nimport * as PIXI from \"pixi.js\";\nimport { PrebuiltComponentAnimations } from \"./components/animations\";\nimport {\n PredictionController,\n type PredictionHistoryEntry,\n type PredictionState,\n} from \"@rpgjs/common\";\nimport { NotificationManager } from \"./Gui/NotificationManager\";\nimport { SaveClientService } from \"./services/save\";\n\ninterface MovementTrajectoryPoint {\n frame: number;\n tick: number;\n timestamp: number;\n input: Direction;\n x: number;\n y: number;\n direction?: Direction;\n}\n\nexport class RpgClientEngine<T = any> {\n private guiService: RpgGui;\n private webSocket: AbstractWebsocket;\n private loadMapService: LoadMapService;\n private hooks: Hooks;\n private sceneMap: RpgClientMap\n private selector: HTMLElement;\n public globalConfig: T;\n public sceneComponent: any;\n stopProcessingInput = false;\n width = signal(\"100%\");\n height = signal(\"100%\");\n spritesheets: Map<string, any> = new Map();\n sounds: Map<string, any> = new Map();\n componentAnimations: any[] = [];\n private spritesheetResolver?: (id: string) => any | Promise<any>;\n private soundResolver?: (id: string) => any | Promise<any>;\n particleSettings: {\n emitters: any[]\n } = {\n emitters: []\n }\n renderer: PIXI.Renderer;\n tick: Observable<number>;\n private canvasApp?: any;\n private canvasElement?: any;\n playerIdSignal = signal<string | null>(null);\n spriteComponentsBehind = signal<any[]>([]);\n spriteComponentsInFront = signal<any[]>([]);\n /** ID of the sprite that the camera should follow. null means follow the current player */\n cameraFollowTargetId = signal<string | null>(null);\n /** Trigger for map shake animation */\n mapShakeTrigger = trigger();\n\n controlsReady = signal(undefined); \n gamePause = signal(false);\n\n private predictionEnabled = false;\n private prediction?: PredictionController<Direction>;\n private readonly SERVER_CORRECTION_THRESHOLD = 30;\n private inputFrameCounter = 0;\n private pendingPredictionFrames: number[] = [];\n private lastClientPhysicsStepAt = 0;\n private frameOffset = 0;\n // Ping/Pong for RTT measurement\n private rtt: number = 0; // Round-trip time in ms\n private pingInterval: any = null;\n private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds\n private lastInputTime = 0;\n private readonly MOVE_PATH_RESEND_INTERVAL_MS = 120;\n private readonly MAX_MOVE_TRAJECTORY_POINTS = 240;\n private lastMovePathSentAt = 0;\n private lastMovePathSentFrame = 0;\n // Track map loading state for onAfterLoading hook using RxJS\n private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);\n private playerIdReceived$ = new BehaviorSubject<boolean>(false);\n private playersReceived$ = new BehaviorSubject<boolean>(false);\n private eventsReceived$ = new BehaviorSubject<boolean>(false);\n private onAfterLoadingSubscription?: any;\n private sceneResetQueued = false;\n \n // Store subscriptions and event listeners for cleanup\n private tickSubscriptions: any[] = [];\n private resizeHandler?: () => void;\n private notificationManager: NotificationManager = new NotificationManager();\n\n constructor(public context) {\n this.webSocket = inject(WebSocketToken);\n this.guiService = inject(RpgGui);\n this.loadMapService = inject(LoadMapToken);\n this.hooks = inject<Hooks>(ModulesToken);\n this.globalConfig = inject(GlobalConfigToken)\n\n if (!this.globalConfig) {\n this.globalConfig = {} as T\n }\n if (!(this.globalConfig as any).box) {\n (this.globalConfig as any).box = {\n styles: {\n backgroundColor: \"#1a1a2e\",\n backgroundOpacity: 0.9\n },\n sounds: {}\n }\n }\n\n this.addComponentAnimation({\n id: \"animation\",\n component: PrebuiltComponentAnimations.Animation\n })\n\n this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;\n this.initializePredictionController();\n }\n\n /**\n * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context\n * \n * This method registers a KeyboardControls instance from CanvasEngine into the DI container,\n * making it available for injection throughout the application. The particularity is that\n * this method is automatically called when a sprite is displayed on the map, allowing the\n * controls to be automatically associated with the active sprite.\n * \n * ## Design\n * \n * - The instance is stored in the DI context under the `KeyboardControls` token\n * - It's automatically assigned when a sprite component mounts (in `character.ce`)\n * - The controls instance comes from the CanvasEngine component's directives\n * - Once registered, it can be retrieved using `inject(KeyboardControls)` from anywhere\n * \n * @param controlInstance - The CanvasEngine KeyboardControls instance to register\n * \n * @example\n * ```ts\n * // The method is automatically called when a sprite is displayed:\n * // client.setKeyboardControls(element.directives.controls)\n * \n * // Later, retrieve and use the controls instance:\n * import { Input, inject, KeyboardControls } from '@rpgjs/client'\n * \n * const controls = inject(KeyboardControls)\n * const control = controls.getControl(Input.Enter)\n * \n * if (control) {\n * console.log(control.actionName) // 'action'\n * }\n * ```\n */\n setKeyboardControls(controlInstance: any) {\n const currentValues = this.context.values['inject:' + 'KeyboardControls']\n this.context.values['inject:' + 'KeyboardControls'] = {\n ...currentValues,\n values: new Map([['__default__', controlInstance]])\n }\n this.controlsReady.set(undefined);\n }\n\n async start() {\n this.sceneMap = new RpgClientMap()\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic();\n this.selector = document.body.querySelector(\"#rpg\") as HTMLElement;\n\n const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;\n const { app, canvasElement } = await bootstrapCanvas(\n this.selector,\n Canvas,\n bootstrapOptions\n );\n this.canvasApp = app;\n this.canvasElement = canvasElement;\n this.renderer = app.renderer as PIXI.Renderer;\n this.tick = canvasElement?.propObservables?.context['tick'].observable\n\n const inputCheckSubscription = this.tick.subscribe(() => {\n if (Date.now() - this.lastInputTime > 100) {\n const player = this.getCurrentPlayer();\n if (!player) return;\n (this.sceneMap as any).stopMovement(player);\n }\n });\n this.tickSubscriptions.push(inputCheckSubscription);\n\n\n this.hooks.callHooks(\"client-spritesheets-load\", this).subscribe();\n this.hooks.callHooks(\"client-spritesheetResolver-load\", this).subscribe();\n this.hooks.callHooks(\"client-sounds-load\", this).subscribe();\n this.hooks.callHooks(\"client-soundResolver-load\", this).subscribe();\n \n RpgSound.init(this);\n RpgResource.init(this);\n this.hooks.callHooks(\"client-gui-load\", this).subscribe();\n this.hooks.callHooks(\"client-particles-load\", this).subscribe();\n this.hooks.callHooks(\"client-componentAnimations-load\", this).subscribe();\n this.hooks.callHooks(\"client-sprite-load\", this).subscribe();\n\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onStart\", this));\n\n // wondow is resize\n this.resizeHandler = () => {\n this.hooks.callHooks(\"client-engine-onWindowResize\", this).subscribe();\n };\n window.addEventListener('resize', this.resizeHandler);\n\n const tickSubscription = this.tick.subscribe((tick) => {\n this.stepClientPhysicsTick();\n this.flushPendingPredictedStates();\n this.flushPendingMovePath();\n this.hooks.callHooks(\"client-engine-onStep\", this, tick).subscribe();\n\n // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)\n if (tick % 60 === 0) {\n const now = Date.now();\n this.prediction?.cleanup(now);\n this.prediction?.tryApplyPendingSnapshot();\n }\n });\n this.tickSubscriptions.push(tickSubscription);\n\n await this.webSocket.connection(() => {\n const saveClient = inject(SaveClientService);\n saveClient.initialize(this.webSocket);\n this.initListeners()\n this.guiService._initialize()\n this.startPingPong();\n });\n }\n\n private prepareSyncPayload(data: any): any {\n const payload = { ...(data ?? {}) };\n delete payload.ack;\n delete payload.timestamp;\n\n const myId = this.playerIdSignal();\n const players = payload.players;\n const shouldMaskLocalPosition =\n this.predictionEnabled && !!this.prediction?.hasPendingInputs();\n if (shouldMaskLocalPosition && myId && players && players[myId]) {\n const localPatch = { ...players[myId] };\n delete localPatch.x;\n delete localPatch.y;\n delete localPatch.direction;\n delete localPatch._frames;\n payload.players = {\n ...players,\n [myId]: localPatch,\n };\n }\n\n return payload;\n }\n\n private normalizeAckWithSyncState(\n ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },\n syncData: any,\n ): { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction } {\n const myId = this.playerIdSignal();\n if (!myId) {\n return ack;\n }\n\n const localPatch = syncData?.players?.[myId];\n if (typeof localPatch?.x !== \"number\" || typeof localPatch?.y !== \"number\") {\n return ack;\n }\n\n return {\n ...ack,\n x: localPatch.x,\n y: localPatch.y,\n direction: localPatch.direction ?? ack.direction,\n };\n }\n\n private initListeners() {\n this.webSocket.on(\"sync\", (data) => {\n if (data.pId) {\n this.playerIdSignal.set(data.pId);\n // Signal that player ID was received\n this.playerIdReceived$.next(true);\n }\n\n if (this.sceneResetQueued) {\n this.sceneMap.reset();\n this.sceneMap.loadPhysic();\n this.sceneResetQueued = false;\n }\n\n // Apply client-side prediction filtering and server reconciliation\n this.hooks.callHooks(\"client-sceneMap-onChanges\", this.sceneMap, { partial: data }).subscribe();\n\n const ack = data?.ack;\n const normalizedAck =\n ack && typeof ack.frame === \"number\"\n ? this.normalizeAckWithSyncState(ack, data)\n : undefined;\n const payload = this.prepareSyncPayload(data);\n load(this.sceneMap, payload, true);\n\n if (normalizedAck) {\n this.applyServerAck(normalizedAck);\n }\n\n for (const playerId in payload.players ?? {}) {\n const player = payload.players[playerId]\n if (!player._param) continue\n for (const param in player._param) {\n this.sceneMap.players()[playerId]._param()[param] = player._param[param]\n }\n }\n \n // Check if players and events are present in sync data\n const players = payload.players || this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = payload.events || this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n });\n\n // Handle pong responses for RTT measurement\n this.webSocket.on(\"pong\", (data: { serverTick: number; clientFrame: number; clientTime: number }) => {\n const now = Date.now();\n this.rtt = now - data.clientTime;\n\n // Calculate frame offset: how many ticks ahead the server is compared to our frame counter\n // This helps us estimate which server tick corresponds to each client input frame\n const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT\n const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;\n\n // Update frame offset (only if we have inputs to calibrate with)\n if (this.inputFrameCounter > 0) {\n this.frameOffset = estimatedServerTickNow - data.clientFrame;\n }\n\n console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);\n });\n\n this.webSocket.on(\"changeMap\", (data) => {\n this.sceneResetQueued = true;\n // Reset camera follow to default (follow current player) when changing maps\n this.cameraFollowTargetId.set(null);\n const transferToken = typeof data?.transferToken === \"string\" ? data.transferToken : undefined;\n this.loadScene(data.mapId, transferToken);\n });\n\n this.webSocket.on(\"showComponentAnimation\", (data) => {\n const { params, object, position, id } = data;\n if (!object && position === undefined) {\n throw new Error(\"Please provide an object or x and y coordinates\");\n }\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n this.getComponentAnimation(id).displayEffect(params, player || position)\n });\n\n this.webSocket.on(\"notification\", (data) => {\n this.notificationManager.add(data);\n });\n\n this.webSocket.on(\"setAnimation\", (data) => {\n const {\n animationName,\n nbTimes,\n object,\n graphic,\n restoreAnimationName,\n restoreGraphics,\n } = data;\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n if (!player) return;\n const restoreOptions = {\n restoreAnimationName,\n restoreGraphics,\n };\n if (graphic !== undefined) {\n player.setAnimation(animationName, graphic, nbTimes, restoreOptions);\n } else {\n player.setAnimation(animationName, nbTimes, restoreOptions);\n }\n })\n\n this.webSocket.on(\"playSound\", (data) => {\n const { soundId, volume, loop } = data;\n this.playSound(soundId, { volume, loop });\n });\n\n this.webSocket.on(\"stopSound\", (data) => {\n const { soundId } = data;\n this.stopSound(soundId);\n });\n\n this.webSocket.on(\"stopAllSounds\", () => {\n this.stopAllSounds();\n });\n\n this.webSocket.on(\"cameraFollow\", (data) => {\n const { targetId, smoothMove } = data;\n this.setCameraFollow(targetId, smoothMove);\n });\n\n this.webSocket.on(\"flash\", (data) => {\n const { object, type, duration, cycles, alpha, tint } = data;\n const sprite = object ? this.sceneMap.getObjectById(object) : undefined;\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash({ type, duration, cycles, alpha, tint });\n }\n });\n\n this.webSocket.on(\"shakeMap\", (data) => {\n const { intensity, duration, frequency, direction } = data || {};\n (this.mapShakeTrigger as any).start({\n intensity,\n duration,\n frequency,\n direction\n });\n });\n\n this.webSocket.on(\"weatherState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n if (raw === null) {\n this.sceneMap.weatherState.set(null);\n return;\n }\n\n const validEffects = [\"rain\", \"snow\", \"fog\", \"cloud\"];\n if (!raw || !validEffects.includes((raw as any).effect)) {\n return;\n }\n\n this.sceneMap.weatherState.set({\n effect: (raw as any).effect,\n preset: (raw as any).preset,\n params: (raw as any).params,\n transitionMs: (raw as any).transitionMs,\n durationMs: (raw as any).durationMs,\n startedAt: (raw as any).startedAt,\n seed: (raw as any).seed,\n });\n });\n\n this.webSocket.on('open', () => {\n this.hooks.callHooks(\"client-engine-onConnected\", this, this.socket).subscribe();\n // Start ping/pong for synchronization\n this.startPingPong();\n })\n\n this.webSocket.on('close', () => {\n this.hooks.callHooks(\"client-engine-onDisconnected\", this, this.socket).subscribe();\n // Stop ping/pong when disconnected\n this.stopPingPong();\n })\n\n this.webSocket.on('error', (error) => {\n this.hooks.callHooks(\"client-engine-onConnectError\", this, error, this.socket).subscribe();\n })\n }\n\n /**\n * Start periodic ping/pong for client-server synchronization\n * \n * Sends ping requests to the server to measure round-trip time (RTT) and\n * calculate the frame offset between client and server ticks.\n * \n * ## Design\n * \n * - Sends ping every 5 seconds\n * - Measures RTT for latency compensation\n * - Calculates frame offset to map client frames to server ticks\n * - Used for accurate server reconciliation\n * \n * @example\n * ```ts\n * // Called automatically when connection opens\n * this.startPingPong();\n * ```\n */\n private startPingPong(): void {\n // Stop existing interval if any\n this.stopPingPong();\n\n // Send initial ping immediately\n this.sendPing();\n\n // Set up periodic pings\n this.pingInterval = setInterval(() => {\n this.sendPing();\n }, this.PING_INTERVAL_MS);\n }\n\n /**\n * Stop periodic ping/pong\n * \n * Stops the ping interval when disconnecting or changing maps.\n * \n * @example\n * ```ts\n * // Called automatically when connection closes\n * this.stopPingPong();\n * ```\n */\n private stopPingPong(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Send a ping request to the server\n * \n * Sends current client time and frame counter to the server,\n * which will respond with its server tick for synchronization.\n * \n * @example\n * ```ts\n * // Send a ping to measure RTT\n * this.sendPing();\n * ```\n */\n private sendPing(): void {\n const clientTime = Date.now();\n const clientFrame = this.getPhysicsTick();\n\n this.webSocket.emit('ping', {\n clientTime,\n clientFrame\n });\n }\n\n private async loadScene(mapId: string, transferToken?: string) {\n await lastValueFrom(this.hooks.callHooks(\"client-sceneMap-onBeforeLoading\", this.sceneMap));\n\n // Clear client prediction states when changing maps\n this.clearClientPredictionStates();\n\n // Reset all conditions for new map loading\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n\n // Unsubscribe previous subscription if exists\n if (this.onAfterLoadingSubscription) {\n this.onAfterLoadingSubscription.unsubscribe();\n }\n\n // Setup RxJS observable to wait for all conditions\n this.setupOnAfterLoadingObserver();\n\n this.webSocket.updateProperties({\n room: mapId,\n query: transferToken ? { transferToken } : undefined,\n })\n await this.webSocket.reconnect(() => {\n const saveClient = inject(SaveClientService);\n saveClient.initialize(this.webSocket);\n this.initListeners()\n this.guiService._initialize()\n })\n const res = await this.loadMapService.load(mapId)\n this.sceneMap.data.set(res)\n \n // Check if playerId is already present\n if (this.playerIdSignal()) {\n this.playerIdReceived$.next(true);\n }\n \n // Check if players and events are already present in sceneMap\n const players = this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n \n // Signal that map loading is completed (this should be last to ensure other checks are done)\n this.mapLoadCompleted$.next(true);\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic()\n }\n\n addSpriteSheet<T = any>(spritesheetClass: any, id?: string): any {\n this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);\n return spritesheetClass as any;\n }\n\n /**\n * Set a resolver function for spritesheets\n * \n * The resolver is called when a spritesheet is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSpritesheetResolver((id) => {\n * if (id === 'dynamic-sprite') {\n * return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSpritesheetResolver(async (id) => {\n * const response = await fetch(`/api/spritesheets/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {\n this.spritesheetResolver = resolver;\n }\n\n /**\n * Get a spritesheet by ID, using resolver if not found in cache\n * \n * This method first checks if the spritesheet exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the spritesheet.\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param id - The spritesheet ID to retrieve\n * @returns The spritesheet if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const spritesheet = engine.getSpriteSheet('my-sprite');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');\n * ```\n */\n getSpriteSheet(id: string): any | Promise<any> {\n // Check cache first\n if (this.spritesheets.has(id)) {\n return this.spritesheets.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.spritesheetResolver) {\n const result = this.spritesheetResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((spritesheet) => {\n if (spritesheet) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, spritesheet);\n }\n return spritesheet;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Add a sound to the engine\n * \n * Adds a sound to the engine's sound cache. The sound can be:\n * - A simple object with `id` and `src` properties\n * - A Howler instance\n * - An object with a `play()` method\n * \n * If the sound has a `src` property, a Howler instance will be created automatically.\n * \n * @param sound - The sound object or Howler instance\n * @param id - Optional sound ID (if not provided, uses sound.id)\n * @returns The added sound\n * \n * @example\n * ```ts\n * // Simple sound object\n * engine.addSound({ id: 'click', src: 'click.mp3' });\n * \n * // With explicit ID\n * engine.addSound({ src: 'music.mp3' }, 'background-music');\n * ```\n */\n addSound(sound: any, id?: string): any {\n const soundId = id || sound.id;\n \n if (!soundId) {\n console.warn('Sound added without an ID. It will not be retrievable.');\n return sound;\n }\n\n // If sound has a src property, create a Howler instance\n if (sound.src && typeof sound.src === 'string') {\n const howlOptions: any = {\n src: [sound.src],\n loop: sound.loop || false,\n volume: sound.volume !== undefined ? sound.volume : 1.0,\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n this.sounds.set(soundId, howl);\n return howl;\n }\n\n // If sound already has a play method (Howler instance or custom), use it directly\n if (sound && typeof sound.play === 'function') {\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n // Otherwise, store as-is\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n /**\n * Set a resolver function for sounds\n * \n * The resolver is called when a sound is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved sound is automatically cached for future use.\n * \n * @param resolver - Function that takes a sound ID and returns a sound or Promise of sound\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSoundResolver((id) => {\n * if (id === 'dynamic-sound') {\n * return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSoundResolver(async (id) => {\n * const response = await fetch(`/api/sounds/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSoundResolver(resolver: (id: string) => any | Promise<any>): void {\n this.soundResolver = resolver;\n }\n\n /**\n * Get a sound by ID, using resolver if not found in cache\n * \n * This method first checks if the sound exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the sound.\n * The resolved sound is automatically cached for future use.\n * \n * @param id - The sound ID to retrieve\n * @returns The sound if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const sound = engine.getSound('my-sound');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const sound = await engine.getSound('dynamic-sound');\n * ```\n */\n getSound(id: string): any | Promise<any> {\n // Check cache first\n if (this.sounds.has(id)) {\n return this.sounds.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.soundResolver) {\n const result = this.soundResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((sound) => {\n if (sound) {\n // Cache the resolved sound\n this.sounds.set(id, sound);\n }\n return sound;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved sound\n this.sounds.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Play a sound by its ID\n * \n * This method retrieves a sound from the cache or resolver and plays it.\n * If the sound is not found, it will attempt to resolve it using the soundResolver.\n * Uses Howler.js for audio playback instead of native Audio elements.\n * \n * @param soundId - The sound ID to play\n * @param options - Optional sound configuration\n * @param options.volume - Volume level (0.0 to 1.0, overrides sound default)\n * @param options.loop - Whether the sound should loop (overrides sound default)\n * \n * @example\n * ```ts\n * // Play a sound synchronously\n * engine.playSound('item-pickup');\n * \n * // Play a sound with volume and loop\n * engine.playSound('background-music', { volume: 0.5, loop: true });\n * \n * // Play a sound asynchronously (when resolver returns Promise)\n * await engine.playSound('dynamic-sound', { volume: 0.8 });\n * ```\n */\n async playSound(soundId: string, options?: { volume?: number; loop?: boolean }): Promise<void> {\n const sound = await this.getSound(soundId);\n if (sound && sound.play) {\n // Sound is already a Howler instance or has a play method\n const howlSoundId = sound._sounds?.[0]?._id;\n \n // Apply volume if provided\n if (options?.volume !== undefined) {\n if (howlSoundId !== undefined) {\n sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);\n } else {\n sound.volume(Math.max(0, Math.min(1, options.volume)));\n }\n }\n \n // Apply loop if provided\n if (options?.loop !== undefined) {\n if (howlSoundId !== undefined) {\n sound.loop(options.loop, howlSoundId);\n } else {\n sound.loop(options.loop);\n }\n }\n \n if (howlSoundId !== undefined) {\n sound.play(howlSoundId);\n } else {\n sound.play();\n }\n } else if (sound && sound.src) {\n // If sound is just a source URL, create a Howler instance and cache it\n const howlOptions: any = {\n src: [sound.src],\n loop: options?.loop !== undefined ? options.loop : (sound.loop || false),\n volume: options?.volume !== undefined ? Math.max(0, Math.min(1, options.volume)) : (sound.volume !== undefined ? sound.volume : 1.0),\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n \n // Cache the Howler instance for future use\n this.sounds.set(soundId, howl);\n \n // Play the sound\n howl.play();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be played`);\n }\n }\n\n /**\n * Stop a sound that is currently playing\n * \n * This method stops a sound that was previously started with `playSound()`.\n * \n * @param soundId - The sound ID to stop\n * \n * @example\n * ```ts\n * // Start a looping sound\n * engine.playSound('background-music', { loop: true });\n * \n * // Later, stop it\n * engine.stopSound('background-music');\n * ```\n */\n stopSound(soundId: string): void {\n const sound = this.sounds.get(soundId);\n if (sound && sound.stop) {\n sound.stop();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be stopped`);\n }\n }\n\n /**\n * Stop all currently playing sounds\n * \n * This method stops all sounds that are currently playing.\n * Useful when changing maps to prevent sound overlap.\n * \n * @example\n * ```ts\n * // Stop all sounds\n * engine.stopAllSounds();\n * ```\n */\n stopAllSounds(): void {\n this.sounds.forEach((sound) => {\n if (sound && sound.stop) {\n sound.stop();\n }\n });\n }\n\n /**\n * Set the camera to follow a specific sprite\n * \n * This method changes which sprite the camera viewport should follow.\n * The camera will smoothly animate to the target sprite if smoothMove options are provided.\n * \n * ## Design\n * \n * The camera follow target is stored in a signal that is read by sprite components.\n * Each sprite checks if it should be followed by comparing its ID with the target ID.\n * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's\n * viewport system.\n * \n * @param targetId - The ID of the sprite to follow. Set to null to follow the current player\n * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease\n * @param smoothMove.time - Duration of the animation in milliseconds (optional)\n * @param smoothMove.ease - Easing function name from https://easings.net (optional)\n * \n * @example\n * ```ts\n * // Follow another player with default smooth animation\n * engine.setCameraFollow(otherPlayerId, true);\n * \n * // Follow an event with custom smooth animation\n * engine.setCameraFollow(eventId, {\n * time: 1000,\n * ease: \"easeInOutQuad\"\n * });\n * \n * // Follow without animation (instant)\n * engine.setCameraFollow(targetId, false);\n * \n * // Return to following current player\n * engine.setCameraFollow(null);\n * ```\n */\n setCameraFollow(\n targetId: string | null,\n smoothMove?: boolean | { time?: number; ease?: string }\n ): void {\n // Store smoothMove options for potential future use with viewport animation\n // For now, we just set the target ID and let CanvasEngine handle the viewport follow\n // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it\n this.cameraFollowTargetId.set(targetId);\n \n // If smoothMove is an object, we could store it for viewport configuration\n // This would require integration with CanvasEngine's viewport animation system\n if (typeof smoothMove === \"object\" && smoothMove !== null) {\n // Future: Apply smoothMove.time and smoothMove.ease to viewport animation\n // For now, CanvasEngine handles viewport following automatically\n }\n }\n\n addParticle(particle: any) {\n this.particleSettings.emitters.push(particle)\n return particle;\n }\n\n /**\n * Add a component to render behind sprites\n * Components added with this method will be displayed with a lower z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `ShadowComponent`\n * 2. Configuration object: `{ component: LightHalo, props: {...} }`\n * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add behind sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a shadow component behind all sprites\n * engine.addSpriteComponentBehind(ShadowComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentBehind({ \n * component: LightHalo, \n * props: { radius: 30 } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentBehind({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentBehind(component: any) {\n this.spriteComponentsBehind.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component to render in front of sprites\n * Components added with this method will be displayed with a higher z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `HealthBarComponent`\n * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`\n * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add in front of sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a health bar component in front of all sprites\n * engine.addSpriteComponentInFront(HealthBarComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentInFront({ \n * component: StatusIndicator, \n * props: { type: 'poison' } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentInFront({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {\n this.spriteComponentsInFront.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component animation to the engine\n * \n * Component animations are temporary visual effects that can be displayed\n * on sprites or objects, such as hit indicators, spell effects, or status animations.\n * \n * @param componentAnimation - The component animation configuration\n * @param componentAnimation.id - Unique identifier for the animation\n * @param componentAnimation.component - The component function to render\n * @returns The added component animation configuration\n * \n * @example\n * ```ts\n * // Add a hit animation component\n * engine.addComponentAnimation({\n * id: 'hit',\n * component: HitComponent\n * });\n * \n * // Add an explosion effect component\n * engine.addComponentAnimation({\n * id: 'explosion',\n * component: ExplosionComponent\n * });\n * ```\n */\n addComponentAnimation(componentAnimation: {\n component: any,\n id: string\n }) {\n const instance = new AnimationManager()\n this.componentAnimations.push({\n id: componentAnimation.id,\n component: componentAnimation.component,\n instance: instance,\n current: instance.current\n })\n return componentAnimation;\n }\n\n /**\n * Get a component animation by its ID\n * \n * Retrieves the EffectManager instance for a specific component animation,\n * which can be used to display the animation on sprites or objects.\n * \n * @param id - The unique identifier of the component animation\n * @returns The EffectManager instance for the animation\n * @throws Error if the component animation is not found\n * \n * @example\n * ```ts\n * // Get the hit animation and display it\n * const hitAnimation = engine.getComponentAnimation('hit');\n * hitAnimation.displayEffect({ text: \"Critical!\" }, player);\n * ```\n */\n getComponentAnimation(id: string): AnimationManager {\n const componentAnimation = this.componentAnimations.find((componentAnimation) => componentAnimation.id === id)\n if (!componentAnimation) {\n throw new Error(`Component animation with id ${id} not found`)\n }\n return componentAnimation.instance\n }\n\n /**\n * Start a transition\n * \n * Convenience method to display a transition by its ID using the GUI system.\n * \n * @param id - The unique identifier of the transition to start\n * @param props - Props to pass to the transition component\n * \n * @example\n * ```ts\n * // Start a fade transition\n * engine.startTransition('fade', { duration: 1000, color: 'black' });\n * \n * // Start with onFinish callback\n * engine.startTransition('fade', {\n * duration: 1000,\n * onFinish: () => console.log('Fade complete')\n * });\n * ```\n */\n startTransition(id: string, props: any = {}) {\n if (!this.guiService.exists(id)) {\n throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);\n }\n this.guiService.display(id, props);\n }\n\n async processInput({ input }: { input: Direction }) {\n if (this.stopProcessingInput) return;\n\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n typeof currentPlayer.canMove !== \"function\" ||\n currentPlayer.canMove();\n if (!canMove) {\n this.interruptCurrentPlayerMovement(currentPlayer);\n return;\n }\n\n const timestamp = Date.now();\n let frame: number;\n let tick: number;\n if (this.predictionEnabled && this.prediction) {\n const meta = this.prediction.recordInput(input, timestamp);\n frame = meta.frame;\n tick = meta.tick;\n } else {\n frame = ++this.inputFrameCounter;\n tick = this.getPhysicsTick();\n }\n this.inputFrameCounter = frame;\n this.hooks.callHooks(\"client-engine-onInput\", this, { input, playerId: this.playerId }).subscribe();\n\n const bodyReady = this.ensureCurrentPlayerBody();\n if (currentPlayer && bodyReady) {\n currentPlayer.changeDirection(input);\n (this.sceneMap as any).moveBody(currentPlayer, input);\n if (this.predictionEnabled && this.prediction) {\n this.pendingPredictionFrames.push(frame);\n if (this.pendingPredictionFrames.length > 240) {\n this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);\n }\n }\n }\n\n this.emitMovePacket(input, frame, tick, timestamp, true);\n this.lastInputTime = Date.now();\n }\n\n processAction({ action }: { action: number }) {\n if (this.stopProcessingInput) return;\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n typeof currentPlayer.canMove !== \"function\" ||\n currentPlayer.canMove();\n if (!canMove) return;\n\n this.hooks.callHooks(\"client-engine-onInput\", this, { input: 'action', playerId: this.playerId }).subscribe();\n this.webSocket.emit('action', { action })\n }\n\n get PIXI() {\n return PIXI\n }\n\n get socket() {\n return this.webSocket\n }\n\n get playerId() {\n return this.playerIdSignal()\n }\n\n get scene() {\n return this.sceneMap\n }\n\n private getPhysicsTick(): number {\n return this.sceneMap?.getTick?.() ?? 0;\n }\n\n private ensureCurrentPlayerBody(): boolean {\n const player = this.sceneMap?.getCurrentPlayer();\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return false;\n }\n if (!player.id) {\n player.id = myId;\n }\n if (this.sceneMap.getBody(myId)) {\n return true;\n }\n try {\n this.sceneMap.loadPhysic();\n } catch (error) {\n console.error(\"[RPGJS] Unable to initialize client physics before input:\", error);\n return false;\n }\n return !!this.sceneMap.getBody(myId);\n }\n\n private stepClientPhysicsTick(): void {\n if (!this.predictionEnabled || !this.sceneMap) {\n return;\n }\n const now = Date.now();\n if (this.lastClientPhysicsStepAt === 0) {\n this.lastClientPhysicsStepAt = now;\n }\n const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));\n this.lastClientPhysicsStepAt = now;\n this.sceneMap.stepClientPhysics(deltaMs);\n }\n\n private flushPendingPredictedStates(): void {\n if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {\n return;\n }\n const state = this.getLocalPlayerState();\n while (this.pendingPredictionFrames.length > 0) {\n const frame = this.pendingPredictionFrames.shift();\n if (typeof frame === \"number\") {\n this.prediction.attachPredictedState(frame, state);\n }\n }\n }\n\n private buildPendingMoveTrajectory(): MovementTrajectoryPoint[] {\n if (!this.predictionEnabled || !this.prediction) {\n return [];\n }\n const pendingInputs = this.prediction.getPendingInputs();\n const trajectory: MovementTrajectoryPoint[] = [];\n for (const entry of pendingInputs) {\n const state = entry.state;\n if (!state) continue;\n if (typeof state.x !== \"number\" || typeof state.y !== \"number\") continue;\n trajectory.push({\n frame: entry.frame,\n tick: entry.tick,\n timestamp: entry.timestamp,\n input: entry.direction,\n x: state.x,\n y: state.y,\n direction: state.direction ?? entry.direction,\n });\n }\n if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {\n return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);\n }\n return trajectory;\n }\n\n private emitMovePacket(\n input: Direction,\n frame: number,\n tick: number,\n timestamp: number,\n force = false,\n ): void {\n const trajectory = this.buildPendingMoveTrajectory();\n const latestTrajectoryFrame =\n trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;\n const shouldThrottle =\n !force &&\n latestTrajectoryFrame <= this.lastMovePathSentFrame &&\n timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;\n if (shouldThrottle) {\n return;\n }\n\n this.webSocket.emit(\"move\", {\n input,\n timestamp,\n frame,\n tick,\n trajectory,\n });\n this.lastMovePathSentAt = timestamp;\n this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);\n }\n\n private flushPendingMovePath(): void {\n if (!this.predictionEnabled || !this.prediction) {\n return;\n }\n const player = this.sceneMap?.getCurrentPlayer?.() as any;\n if (\n player &&\n typeof player.canMove === \"function\" &&\n !player.canMove()\n ) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n const pendingInputs = this.prediction.getPendingInputs();\n if (pendingInputs.length === 0) {\n return;\n }\n const latest = pendingInputs[pendingInputs.length - 1];\n if (!latest) {\n return;\n }\n const now = Date.now();\n if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {\n return;\n }\n this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);\n }\n\n private getLocalPlayerState(): PredictionState<Direction> {\n const currentPlayer = this.sceneMap?.getCurrentPlayer();\n if (!currentPlayer) {\n return { x: 0, y: 0, direction: Direction.Down };\n }\n const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, \"top-left\");\n const x = topLeft?.x ?? currentPlayer.x();\n const y = topLeft?.y ?? currentPlayer.y();\n const direction = currentPlayer.direction();\n return { x, y, direction };\n }\n\n private applyAuthoritativeState(state: PredictionState<Direction>): void {\n const player = this.sceneMap?.getCurrentPlayer();\n if (!player) return;\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(player.id, state.x, state.y, \"top-left\");\n }\n player.x.set(Math.round(state.x));\n player.y.set(Math.round(state.y));\n if (state.direction) {\n player.changeDirection(state.direction);\n }\n }\n\n private initializePredictionController(): void {\n if (!this.predictionEnabled) {\n this.prediction = undefined;\n this.sceneMap?.configureClientPrediction?.(false);\n return;\n }\n const configuredTtl = (this.globalConfig as any)?.prediction?.historyTtlMs;\n const historyTtlMs = typeof configuredTtl === \"number\" ? configuredTtl : 10000;\n const configuredMaxEntries = (this.globalConfig as any)?.prediction?.maxHistoryEntries;\n const maxHistoryEntries =\n typeof configuredMaxEntries === \"number\"\n ? configuredMaxEntries\n : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);\n this.sceneMap?.configureClientPrediction?.(true);\n this.prediction = new PredictionController<Direction>({\n correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,\n historyTtlMs,\n maxHistoryEntries,\n getPhysicsTick: () => this.getPhysicsTick(),\n getCurrentState: () => this.getLocalPlayerState(),\n setAuthoritativeState: (state) => this.applyAuthoritativeState(state),\n });\n }\n\n getCurrentPlayer() {\n return this.sceneMap.getCurrentPlayer()\n }\n\n emitSceneMapHook(hookName: string, ...args: any[]): void {\n this.hooks.callHooks(`client-sceneMap-${hookName}`, ...args).subscribe();\n }\n\n /**\n * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook\n * \n * This method uses RxJS `combineLatest` to wait for all conditions to be met,\n * regardless of the order in which they arrive:\n * 1. The map loading is completed (loadMapService.load is finished)\n * 2. We received a player ID (pId)\n * 3. Players array has at least one element\n * 4. Events property is present in the sync data\n * \n * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.\n * \n * ## Design\n * \n * Uses BehaviorSubjects to track each condition state, allowing events to arrive\n * in any order. The `combineLatest` operator waits until all observables emit `true`,\n * then `take(1)` ensures the hook is called only once, and `switchMap` handles\n * the hook execution.\n * \n * @example\n * ```ts\n * // Called automatically in loadScene to setup the observer\n * this.setupOnAfterLoadingObserver();\n * ```\n */\n private setupOnAfterLoadingObserver(): void {\n this.onAfterLoadingSubscription = combineLatest([\n this.mapLoadCompleted$.pipe(filter(completed => completed === true)),\n this.playerIdReceived$.pipe(filter(received => received === true)),\n this.playersReceived$.pipe(filter(received => received === true)),\n this.eventsReceived$.pipe(filter(received => received === true))\n ]).pipe(\n take(1), // Only execute once when all conditions are met\n switchMap(() => {\n // Call the hook and return the observable\n return this.hooks.callHooks(\"client-sceneMap-onAfterLoading\", this.sceneMap);\n })\n ).subscribe();\n }\n\n /**\n * Clear client prediction states for cleanup\n * \n * Removes old prediction states and input history to prevent memory leaks.\n * Should be called when changing maps or disconnecting.\n * \n * @example\n * ```ts\n * // Clear prediction states when changing maps\n * engine.clearClientPredictionStates();\n * ```\n */\n clearClientPredictionStates() {\n this.initializePredictionController();\n this.frameOffset = 0;\n this.inputFrameCounter = 0;\n this.pendingPredictionFrames = [];\n this.lastClientPhysicsStepAt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n }\n\n /**\n * Stop local movement immediately and discard pending predicted movement.\n *\n * Use this before a blocking action such as an A-RPG attack, dialog, dash\n * startup, or any client-side state where already buffered movement inputs\n * must not be replayed after server reconciliation.\n *\n * @param player - Player object to stop. Defaults to the current player.\n * @returns `true` when a player was found and interrupted.\n *\n * @example\n * ```ts\n * engine.interruptCurrentPlayerMovement();\n * ```\n */\n interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {\n if (!player) {\n return false;\n }\n (this.sceneMap as any)?.stopMovement?.(player);\n this.prediction?.clearPendingInputs();\n this.pendingPredictionFrames = [];\n this.lastInputTime = 0;\n this.lastMovePathSentAt = Date.now();\n this.lastMovePathSentFrame = this.inputFrameCounter;\n return true;\n }\n\n /**\n * Trigger a flash animation on a sprite\n * \n * This method allows you to trigger a flash effect on any sprite from client-side code.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash is applied directly to the sprite object using its flash trigger.\n * This is useful for client-side visual feedback, UI interactions, or local effects\n * that don't need to be synchronized with the server.\n * \n * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player\n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Flash the current player with default settings\n * engine.flash();\n * \n * // Flash a specific sprite with red tint\n * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint for dramatic effect\n * engine.flash(undefined, { \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash on current player\n * engine.flash(undefined, { \n * type: 'tint', \n * tint: 'red', \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(\n spriteId?: string,\n options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }\n ): void {\n const targetId = spriteId || this.playerId;\n if (!targetId) return;\n\n const sprite = this.sceneMap.getObjectById(targetId);\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash(options);\n }\n }\n\n private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {\n if (this.predictionEnabled && this.prediction) {\n const result = this.prediction.applyServerAck({\n frame: ack.frame,\n serverTick: ack.serverTick,\n state:\n typeof ack.x === \"number\" && typeof ack.y === \"number\"\n ? { x: ack.x, y: ack.y, direction: ack.direction }\n : undefined,\n });\n if (result.state && result.needsReconciliation) {\n this.reconcilePrediction(result.state, result.pendingInputs);\n }\n return;\n }\n\n if (typeof ack.x !== \"number\" || typeof ack.y !== \"number\") {\n return;\n }\n const player = this.getCurrentPlayer() as any;\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return;\n }\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(myId, ack.x, ack.y, \"top-left\");\n }\n player.x.set(Math.round(ack.x));\n player.y.set(Math.round(ack.y));\n if (ack.direction) {\n player.changeDirection(ack.direction);\n }\n }\n\n private reconcilePrediction(\n authoritativeState: PredictionState<Direction>,\n pendingInputs: PredictionHistoryEntry<Direction>[],\n ): void {\n const player = this.getCurrentPlayer() as any;\n if (!player) {\n return;\n }\n if (typeof player.canMove === \"function\" && !player.canMove()) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n\n (this.sceneMap as any).stopMovement(player);\n this.applyAuthoritativeState(authoritativeState);\n\n if (!pendingInputs.length) {\n return;\n }\n\n // Keep replay bounded while still tolerating high-latency links.\n const replayInputs = pendingInputs.slice(-600);\n for (const entry of replayInputs) {\n if (!entry?.direction) continue;\n (this.sceneMap as any).moveBody(player, entry.direction);\n this.sceneMap.stepPredictionTick();\n this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());\n }\n }\n\n /**\n * Replay unacknowledged inputs from a given frame to resimulate client prediction\n * after applying server authority at a certain frame.\n * \n * @param startFrame - The last server-acknowledged frame\n * \n * @example\n * ```ts\n * // After applying a server correction at frame N\n * this.replayUnackedInputsFromFrame(N);\n * ```\n */\n private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {\n // Prediction controller handles replay internally. Kept for backwards compatibility.\n }\n\n /**\n * Clear all client resources and reset state\n * \n * This method should be called to clean up all client-side resources when\n * shutting down or resetting the client engine. It:\n * - Destroys the PIXI renderer\n * - Stops all sounds\n * - Cleans up subscriptions and event listeners\n * - Resets scene map\n * - Stops ping/pong interval\n * - Clears prediction states\n * \n * ## Design\n * \n * This method is used primarily in testing environments to ensure clean\n * state between tests. In production, the client engine typically persists\n * for the lifetime of the application.\n * \n * @example\n * ```ts\n * // In test cleanup\n * afterEach(() => {\n * clientEngine.clear();\n * });\n * ```\n */\n clear(): void {\n try {\n // First, unsubscribe from all tick subscriptions to stop rendering attempts\n for (const subscription of this.tickSubscriptions) {\n if (subscription && typeof subscription.unsubscribe === 'function') {\n subscription.unsubscribe();\n }\n }\n this.tickSubscriptions = [];\n\n // Stop ping/pong interval\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n\n // Clean up onAfterLoading subscription\n if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {\n this.onAfterLoadingSubscription.unsubscribe();\n this.onAfterLoadingSubscription = undefined;\n }\n\n // Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app\n // This prevents CanvasEngine from trying to render after PIXI is destroyed\n // CanvasEngine manages its own render loop which could try to access PIXI after destruction\n if (this.canvasElement) {\n try {\n // Try to stop or cleanup canvasElement if it has cleanup methods\n if (typeof (this.canvasElement as any).destroy === 'function') {\n (this.canvasElement as any).destroy();\n }\n // Clear the reference\n this.canvasElement = undefined;\n } catch (error) {\n // Ignore errors during canvasElement cleanup\n }\n }\n\n // Reset scene map if it exists (this should stop any ongoing animations/renders)\n if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {\n (this.sceneMap as any).reset(true);\n }\n\n // Stop all sounds\n this.stopAllSounds();\n\n // Remove resize event listener\n if (this.resizeHandler && typeof window !== 'undefined') {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = undefined;\n }\n\n // Destroy PIXI app and renderer if they exist\n // Destroy the app first, which will destroy the renderer\n // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)\n const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';\n \n if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {\n try {\n // Stop the ticker first to prevent any render calls during destruction\n if (this.canvasApp.ticker) {\n if (typeof this.canvasApp.ticker.stop === 'function') {\n this.canvasApp.ticker.stop();\n }\n // Also remove all listeners from ticker to prevent callbacks\n if (typeof this.canvasApp.ticker.removeAll === 'function') {\n this.canvasApp.ticker.removeAll();\n }\n }\n \n // Stop the renderer's ticker if it exists separately\n if (this.renderer && (this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n // Remove the canvas from DOM before destroying to prevent render attempts\n if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {\n this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);\n }\n \n // Destroy with minimal options to avoid issues\n // Don't pass options that might trigger additional cleanup that could fail\n this.canvasApp.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.canvasApp = undefined;\n // canvasApp.destroy() already destroyed the renderer, so just null it\n this.renderer = null as any;\n } else if (rendererStillExists) {\n // Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed\n try {\n // Stop the renderer's ticker if it has one\n if ((this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n this.renderer.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.renderer = null as any;\n }\n\n // Clean up prediction controller\n if (this.prediction) {\n // Prediction controller cleanup is handled internally when destroyed\n this.prediction = undefined;\n }\n\n // Reset signals\n this.playerIdSignal.set(null);\n this.cameraFollowTargetId.set(null);\n this.spriteComponentsBehind.set([]);\n this.spriteComponentsInFront.set([]);\n \n // Clear maps and arrays\n this.spritesheets.clear();\n this.sounds.clear();\n this.componentAnimations = [];\n this.particleSettings.emitters = [];\n\n // Reset state\n this.stopProcessingInput = false;\n this.lastInputTime = 0;\n this.inputFrameCounter = 0;\n this.frameOffset = 0;\n this.rtt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n\n // Reset behavior subjects\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n } catch (error) {\n console.warn('Error during client engine cleanup:', error);\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/client",
3
- "version": "5.0.0-beta.3",
3
+ "version": "5.0.0-beta.4",
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,9 +22,9 @@
22
22
  "pixi.js": "^8.9.2"
23
23
  },
24
24
  "dependencies": {
25
- "@rpgjs/common": "5.0.0-beta.3",
26
- "@rpgjs/server": "5.0.0-beta.3",
27
- "@rpgjs/ui-css": "5.0.0-beta.3",
25
+ "@rpgjs/common": "5.0.0-beta.4",
26
+ "@rpgjs/server": "5.0.0-beta.4",
27
+ "@rpgjs/ui-css": "5.0.0-beta.4",
28
28
  "@signe/di": "^2.9.0",
29
29
  "@signe/room": "^2.9.0",
30
30
  "@signe/sync": "^2.9.0",
@@ -1,6 +1,6 @@
1
1
  import { Hooks, ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
2
2
  import { trigger, signal, effect } from "canvasengine";
3
- import { filter, from, map, Subscription, switchMap } from "rxjs";
3
+ import { filter, from, map, of, Subscription, switchMap } from "rxjs";
4
4
  import { inject } from "../core/inject";
5
5
  import { RpgClientEngine } from "../RpgClientEngine";
6
6
  import TextComponent from "../components/dynamics/text.ce";
@@ -11,6 +11,11 @@ const DYNAMIC_COMPONENTS = {
11
11
 
12
12
  type Frame = { x: number; y: number; ts: number };
13
13
 
14
+ type AnimationRestoreOptions = {
15
+ restoreAnimationName?: string;
16
+ restoreGraphics?: any[];
17
+ };
18
+
14
19
  export abstract class RpgClientObject extends RpgCommonPlayer {
15
20
  abstract _type: string;
16
21
  emitParticleTrigger = trigger();
@@ -22,6 +27,10 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
22
27
  graphicsSignals = signal<any[]>([]);
23
28
  _component = {} // temporary component memory
24
29
  flashTrigger = trigger();
30
+ private animationRestoreState?: {
31
+ animationName: string;
32
+ graphics: any[];
33
+ };
25
34
 
26
35
  constructor() {
27
36
  super();
@@ -39,8 +48,10 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
39
48
  this.graphics.observable
40
49
  .pipe(
41
50
  map(({ items }) => items),
42
- filter(graphics => graphics.length > 0),
43
- switchMap(graphics => from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic)))))
51
+ switchMap(graphics => {
52
+ if (graphics.length === 0) return of([]);
53
+ return from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic))));
54
+ })
44
55
  )
45
56
  .subscribe((sheets) => {
46
57
  this.graphicsSignals.set(sheets);
@@ -101,6 +112,30 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
101
112
  }
102
113
 
103
114
  private animationSubscription?: Subscription;
115
+ private animationResetTimeout?: ReturnType<typeof setTimeout>;
116
+
117
+ private clearAnimationControls() {
118
+ if (this.animationSubscription) {
119
+ this.animationSubscription.unsubscribe();
120
+ this.animationSubscription = undefined;
121
+ }
122
+ if (this.animationResetTimeout) {
123
+ clearTimeout(this.animationResetTimeout);
124
+ this.animationResetTimeout = undefined;
125
+ }
126
+ }
127
+
128
+ private finishTemporaryAnimation() {
129
+ const restoreState = this.animationRestoreState;
130
+ this.clearAnimationControls();
131
+ this.animationCurrentIndex.set(0);
132
+ if (restoreState) {
133
+ this.animationName.set(restoreState.animationName);
134
+ this.graphics.set([...restoreState.graphics]);
135
+ }
136
+ this.animationRestoreState = undefined;
137
+ this.animationIsPlaying.set(false);
138
+ }
104
139
 
105
140
  /**
106
141
  * Trigger a flash animation on this sprite
@@ -199,12 +234,13 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
199
234
  * ```
200
235
  */
201
236
  resetAnimationState() {
237
+ if (this.animationRestoreState) {
238
+ this.finishTemporaryAnimation();
239
+ return;
240
+ }
202
241
  this.animationIsPlaying.set(false);
203
242
  this.animationCurrentIndex.set(0);
204
- if (this.animationSubscription) {
205
- this.animationSubscription.unsubscribe();
206
- this.animationSubscription = undefined;
207
- }
243
+ this.clearAnimationControls();
208
244
  }
209
245
 
210
246
  /**
@@ -226,7 +262,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
226
262
  * player.setAnimation('spell');
227
263
  * ```
228
264
  */
229
- setAnimation(animationName: string, nbTimes?: number): void;
265
+ setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): void;
230
266
  /**
231
267
  * Set a custom animation with temporary graphic change
232
268
  *
@@ -244,30 +280,52 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
244
280
  * player.setAnimation('attack', 'hero_attack', 3);
245
281
  * ```
246
282
  */
247
- setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;
248
- setAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes?: number): void {
249
- if (this.animationIsPlaying()) return;
250
- this.animationIsPlaying.set(true);
251
- const previousAnimationName = this.animationName();
252
- const previousGraphics = this.graphics();
253
- this.animationCurrentIndex.set(0);
254
-
283
+ setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): void;
284
+ setAnimation(
285
+ animationName: string,
286
+ graphicOrNbTimes?: string | string[] | number,
287
+ nbTimesOrOptions?: number | AnimationRestoreOptions,
288
+ options?: AnimationRestoreOptions
289
+ ): void {
255
290
  let graphic: string | string[] | undefined;
256
291
  let finalNbTimes: number = Infinity;
292
+ let restoreOptions: AnimationRestoreOptions | undefined = options;
257
293
 
258
294
  // Handle overloads
259
295
  if (typeof graphicOrNbTimes === 'number') {
260
296
  // setAnimation(animationName, nbTimes)
261
297
  finalNbTimes = graphicOrNbTimes;
298
+ restoreOptions = typeof nbTimesOrOptions === 'object' ? nbTimesOrOptions : options;
262
299
  } else if (graphicOrNbTimes !== undefined) {
263
300
  // setAnimation(animationName, graphic, nbTimes)
264
301
  graphic = graphicOrNbTimes;
265
- finalNbTimes = nbTimes ?? Infinity;
302
+ if (typeof nbTimesOrOptions === 'number') {
303
+ finalNbTimes = nbTimesOrOptions;
304
+ } else {
305
+ finalNbTimes = Infinity;
306
+ restoreOptions = nbTimesOrOptions ?? options;
307
+ }
266
308
  } else {
267
309
  // setAnimation(animationName) - nbTimes remains Infinity
268
310
  finalNbTimes = Infinity;
269
311
  }
270
312
 
313
+ if (this.animationIsPlaying()) {
314
+ this.finishTemporaryAnimation();
315
+ }
316
+
317
+ this.animationIsPlaying.set(true);
318
+ const previousAnimationName =
319
+ restoreOptions?.restoreAnimationName ?? this.animationName();
320
+ const previousGraphics = restoreOptions?.restoreGraphics
321
+ ? [...restoreOptions.restoreGraphics]
322
+ : [...this.graphics()];
323
+ this.animationRestoreState = {
324
+ animationName: previousAnimationName,
325
+ graphics: previousGraphics,
326
+ };
327
+ this.animationCurrentIndex.set(0);
328
+
271
329
  // Temporarily change graphic if provided
272
330
  if (graphic !== undefined) {
273
331
  if (Array.isArray(graphic)) {
@@ -277,27 +335,23 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
277
335
  }
278
336
  }
279
337
 
280
- // Clean up any existing subscription
281
- if (this.animationSubscription) {
282
- this.animationSubscription.unsubscribe();
283
- }
338
+ this.clearAnimationControls();
284
339
 
285
340
  this.animationSubscription =
286
341
  this.animationCurrentIndex.observable.subscribe((index) => {
287
342
  if (index >= finalNbTimes) {
288
- this.animationCurrentIndex.set(0);
289
- this.animationName.set(previousAnimationName);
290
- // Reset graphic to previous value if it was changed
291
- if (graphic !== undefined) {
292
- this.graphics.set(previousGraphics);
293
- }
294
- this.animationIsPlaying.set(false);
295
- if (this.animationSubscription) {
296
- this.animationSubscription.unsubscribe();
297
- this.animationSubscription = undefined;
298
- }
343
+ this.finishTemporaryAnimation();
299
344
  }
300
345
  });
346
+
347
+ if (finalNbTimes !== Infinity) {
348
+ this.animationResetTimeout = setTimeout(() => {
349
+ if (this.animationIsPlaying()) {
350
+ this.finishTemporaryAnimation();
351
+ }
352
+ }, Math.max(1000, finalNbTimes * 1000));
353
+ }
354
+
301
355
  this.animationName.set(animationName);
302
356
  }
303
357
 
@@ -374,15 +374,27 @@ export class RpgClientEngine<T = any> {
374
374
  this.notificationManager.add(data);
375
375
  });
376
376
 
377
- this.webSocket.on("setAnimation", (data) => {
378
- const { animationName, nbTimes, object, graphic } = data;
379
- const player = this.sceneMap.getObjectById(object);
380
- if (graphic !== undefined) {
381
- player.setAnimation(animationName, graphic, nbTimes);
382
- } else {
383
- player.setAnimation(animationName, nbTimes);
384
- }
385
- })
377
+ this.webSocket.on("setAnimation", (data) => {
378
+ const {
379
+ animationName,
380
+ nbTimes,
381
+ object,
382
+ graphic,
383
+ restoreAnimationName,
384
+ restoreGraphics,
385
+ } = data;
386
+ const player = object ? this.sceneMap.getObjectById(object) : undefined;
387
+ if (!player) return;
388
+ const restoreOptions = {
389
+ restoreAnimationName,
390
+ restoreGraphics,
391
+ };
392
+ if (graphic !== undefined) {
393
+ player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
394
+ } else {
395
+ player.setAnimation(animationName, nbTimes, restoreOptions);
396
+ }
397
+ })
386
398
 
387
399
  this.webSocket.on("playSound", (data) => {
388
400
  const { soundId, volume, loop } = data;
@@ -1174,6 +1186,18 @@ export class RpgClientEngine<T = any> {
1174
1186
  }
1175
1187
 
1176
1188
  async processInput({ input }: { input: Direction }) {
1189
+ if (this.stopProcessingInput) return;
1190
+
1191
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1192
+ const canMove =
1193
+ !currentPlayer ||
1194
+ typeof currentPlayer.canMove !== "function" ||
1195
+ currentPlayer.canMove();
1196
+ if (!canMove) {
1197
+ this.interruptCurrentPlayerMovement(currentPlayer);
1198
+ return;
1199
+ }
1200
+
1177
1201
  const timestamp = Date.now();
1178
1202
  let frame: number;
1179
1203
  let tick: number;
@@ -1188,7 +1212,6 @@ export class RpgClientEngine<T = any> {
1188
1212
  this.inputFrameCounter = frame;
1189
1213
  this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
1190
1214
 
1191
- const currentPlayer = this.sceneMap.getCurrentPlayer();
1192
1215
  const bodyReady = this.ensureCurrentPlayerBody();
1193
1216
  if (currentPlayer && bodyReady) {
1194
1217
  currentPlayer.changeDirection(input);
@@ -1207,6 +1230,13 @@ export class RpgClientEngine<T = any> {
1207
1230
 
1208
1231
  processAction({ action }: { action: number }) {
1209
1232
  if (this.stopProcessingInput) return;
1233
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1234
+ const canMove =
1235
+ !currentPlayer ||
1236
+ typeof currentPlayer.canMove !== "function" ||
1237
+ currentPlayer.canMove();
1238
+ if (!canMove) return;
1239
+
1210
1240
  this.hooks.callHooks("client-engine-onInput", this, { input: 'action', playerId: this.playerId }).subscribe();
1211
1241
  this.webSocket.emit('action', { action })
1212
1242
  }
@@ -1337,6 +1367,15 @@ export class RpgClientEngine<T = any> {
1337
1367
  if (!this.predictionEnabled || !this.prediction) {
1338
1368
  return;
1339
1369
  }
1370
+ const player = this.sceneMap?.getCurrentPlayer?.() as any;
1371
+ if (
1372
+ player &&
1373
+ typeof player.canMove === "function" &&
1374
+ !player.canMove()
1375
+ ) {
1376
+ this.interruptCurrentPlayerMovement(player);
1377
+ return;
1378
+ }
1340
1379
  const pendingInputs = this.prediction.getPendingInputs();
1341
1380
  if (pendingInputs.length === 0) {
1342
1381
  return;
@@ -1475,6 +1514,34 @@ export class RpgClientEngine<T = any> {
1475
1514
  this.lastMovePathSentFrame = 0;
1476
1515
  }
1477
1516
 
1517
+ /**
1518
+ * Stop local movement immediately and discard pending predicted movement.
1519
+ *
1520
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
1521
+ * startup, or any client-side state where already buffered movement inputs
1522
+ * must not be replayed after server reconciliation.
1523
+ *
1524
+ * @param player - Player object to stop. Defaults to the current player.
1525
+ * @returns `true` when a player was found and interrupted.
1526
+ *
1527
+ * @example
1528
+ * ```ts
1529
+ * engine.interruptCurrentPlayerMovement();
1530
+ * ```
1531
+ */
1532
+ interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
1533
+ if (!player) {
1534
+ return false;
1535
+ }
1536
+ (this.sceneMap as any)?.stopMovement?.(player);
1537
+ this.prediction?.clearPendingInputs();
1538
+ this.pendingPredictionFrames = [];
1539
+ this.lastInputTime = 0;
1540
+ this.lastMovePathSentAt = Date.now();
1541
+ this.lastMovePathSentFrame = this.inputFrameCounter;
1542
+ return true;
1543
+ }
1544
+
1478
1545
  /**
1479
1546
  * Trigger a flash animation on a sprite
1480
1547
  *
@@ -1560,7 +1627,7 @@ export class RpgClientEngine<T = any> {
1560
1627
  if (typeof ack.x !== "number" || typeof ack.y !== "number") {
1561
1628
  return;
1562
1629
  }
1563
- const player = this.getCurrentPlayer();
1630
+ const player = this.getCurrentPlayer() as any;
1564
1631
  const myId = this.playerIdSignal();
1565
1632
  if (!player || !myId) {
1566
1633
  return;
@@ -1583,10 +1650,14 @@ export class RpgClientEngine<T = any> {
1583
1650
  authoritativeState: PredictionState<Direction>,
1584
1651
  pendingInputs: PredictionHistoryEntry<Direction>[],
1585
1652
  ): void {
1586
- const player = this.getCurrentPlayer();
1653
+ const player = this.getCurrentPlayer() as any;
1587
1654
  if (!player) {
1588
1655
  return;
1589
1656
  }
1657
+ if (typeof player.canMove === "function" && !player.canMove()) {
1658
+ this.interruptCurrentPlayerMovement(player);
1659
+ return;
1660
+ }
1590
1661
 
1591
1662
  (this.sceneMap as any).stopMovement(player);
1592
1663
  this.applyAuthoritativeState(authoritativeState);