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

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;