@rpgjs/client 5.0.0-beta.2 → 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.
Files changed (80) hide show
  1. package/dist/Game/Map.js +2 -2
  2. package/dist/Game/Object.d.ts +10 -2
  3. package/dist/Game/Object.js +56 -26
  4. package/dist/Game/Object.js.map +1 -1
  5. package/dist/Gui/Gui.js.map +1 -1
  6. package/dist/Gui/NotificationManager.js.map +1 -1
  7. package/dist/RpgClientEngine.d.ts +16 -0
  8. package/dist/RpgClientEngine.js +53 -6
  9. package/dist/RpgClientEngine.js.map +1 -1
  10. package/dist/Sound.js.map +1 -1
  11. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.127.0}/helpers/decorate.js +1 -1
  12. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.127.0}/helpers/decorateMetadata.js +1 -1
  13. package/dist/components/animations/animation.ce.js +2 -4
  14. package/dist/components/animations/animation.ce.js.map +1 -1
  15. package/dist/components/animations/hit.ce.js +17 -24
  16. package/dist/components/animations/hit.ce.js.map +1 -1
  17. package/dist/components/character.ce.js +75 -222
  18. package/dist/components/character.ce.js.map +1 -1
  19. package/dist/components/dynamics/text.ce.js +14 -25
  20. package/dist/components/dynamics/text.ce.js.map +1 -1
  21. package/dist/components/gui/box.ce.js +4 -7
  22. package/dist/components/gui/box.ce.js.map +1 -1
  23. package/dist/components/gui/dialogbox/index.ce.js +37 -54
  24. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  25. package/dist/components/gui/gameover.ce.js +36 -61
  26. package/dist/components/gui/gameover.ce.js.map +1 -1
  27. package/dist/components/gui/hud/hud.ce.js +18 -28
  28. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  29. package/dist/components/gui/menu/equip-menu.ce.js +108 -163
  30. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  31. package/dist/components/gui/menu/exit-menu.ce.js +4 -4
  32. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  33. package/dist/components/gui/menu/items-menu.ce.js +47 -66
  34. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  35. package/dist/components/gui/menu/main-menu.ce.js +58 -77
  36. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  37. package/dist/components/gui/menu/options-menu.ce.js +3 -3
  38. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  39. package/dist/components/gui/menu/skills-menu.ce.js +10 -16
  40. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  41. package/dist/components/gui/mobile/mobile.ce.js +3 -3
  42. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  43. package/dist/components/gui/notification/notification.ce.js +13 -19
  44. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  45. package/dist/components/gui/save-load.ce.js +68 -247
  46. package/dist/components/gui/save-load.ce.js.map +1 -1
  47. package/dist/components/gui/shop/shop.ce.js +84 -123
  48. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  49. package/dist/components/gui/title-screen.ce.js +38 -65
  50. package/dist/components/gui/title-screen.ce.js.map +1 -1
  51. package/dist/components/prebuilt/hp-bar.ce.js +39 -43
  52. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  53. package/dist/components/prebuilt/light-halo.ce.js +33 -58
  54. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  55. package/dist/components/scenes/canvas.ce.js +11 -19
  56. package/dist/components/scenes/canvas.ce.js.map +1 -1
  57. package/dist/components/scenes/draw-map.ce.js +20 -25
  58. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  59. package/dist/components/scenes/event-layer.ce.js +4 -4
  60. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  61. package/dist/core/setup.js.map +1 -1
  62. package/dist/module.js.map +1 -1
  63. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +1 -1
  64. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +1 -1
  65. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +1 -1
  66. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +1 -1
  67. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +1 -1
  68. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +1 -1
  69. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +1 -1
  70. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  71. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +27 -27
  72. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  73. package/dist/services/keyboardControls.js.map +1 -1
  74. package/dist/services/mmorpg.js +2 -1
  75. package/dist/services/mmorpg.js.map +1 -1
  76. package/package.json +7 -7
  77. package/src/Game/Object.ts +86 -32
  78. package/src/RpgClientEngine.ts +83 -12
  79. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
  80. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
package/dist/Game/Map.js CHANGED
@@ -2,8 +2,8 @@ import { inject } from "../core/inject.js";
2
2
  import { sync, users } from "../node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js";
3
3
  import { RpgClientPlayer } from "./Player.js";
4
4
  import { RpgClientEvent } from "./Event.js";
5
- import { __decorateMetadata } from "../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorateMetadata.js";
6
- import { __decorate } from "../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js";
5
+ import { __decorateMetadata } from "../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.js";
6
+ import { __decorate } from "../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.js";
7
7
  import { RpgClientEngine } from "../RpgClientEngine.js";
8
8
  import { computed, signal } from "canvasengine";
9
9
  import { RpgCommonMap } from "@rpgjs/common";
@@ -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":";;;;;;;AAOA,IAAM,qBAAqB,EACzB,MAAM,WACP;AASD,IAAsB,kBAAtB,cAA8C,gBAAgB;CAgB5D,cAAc;AACZ,SAAO;6BAfa,SAAS;sBAChB,OAAO,GAAG;+BACD,OAAO,EAAE;4BACZ,OAAO,MAAM;gBACzB,OAAO,EAAE,CAAC;gBACD,EAAE;yBACF,OAAc,EAAE,CAAC;oBACtB,EAAE;sBACA,SAAS;AAQtB,OAAK,MAAM,UAAU,wBAAwB,KAAK,CAAC,WAAW;AAE9D,OAAK,QAAQ,WAAW,WAAW,EAAE,YAAY;AAC/C,OAAI,CAAC,KAAK,GAAI;GAEd,MAAM,aAAa,MAAM,SAAS,SAChC,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,KAAK,CACpC;AACD,QAAK,SAAS,CAAC,GAAG,KAAK,QAAQ,GAAG,WAAW;IAC7C;AAEF,OAAK,SAAS,WACb,KACC,KAAK,EAAE,YAAY,MAAM,EACzB,WAAU,aAAY;AACpB,OAAI,SAAS,WAAW,EAAG,QAAO,GAAG,EAAE,CAAC;AACxC,UAAO,KAAK,QAAQ,IAAI,SAAS,KAAI,YAAW,KAAK,OAAO,eAAe,QAAQ,CAAC,CAAC,CAAC;IACtF,CACH,CACA,WAAW,WAAW;AACrB,QAAK,gBAAgB,IAAI,OAAO;IAChC;AAEF,OAAK,cAAc,WAClB,KACC,QAAO,UAAS,UAAU,QAAQ,UAAU,KAAA,EAAU,EACtD,KAAK,UAAU,OAAO,UAAU,WAAW,KAAK,MAAM,MAAM,GAAG,MAAM,CACtE,CACA,WAAW,EAAC,iBAAgB;AAC3B,QAAK,MAAM,aAAa,WACtB,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,EAAE;AACpD,SAAK,aAAa;AAClB,YAAQ,IAAI,MAAM;IAClB,MAAM,OAAQ,MAAc;AAC5B,QAAI,mBAAmB,MACrB,MAAK,OAAO,0BAA0B,mBAAmB,MAAM;;IAIrE;AAEF,OAAK,OAAO,KACT,MAEC,CACD,gBAAgB;GACf,MAAM,QAAQ,KAAK,OAAO,OAAO;AACjC,OAAI,OAAO;AACT,QAAI,OAAO,MAAM,MAAM,YAAY,OAAO,MAAM,MAAM,SAAU;AAChE,SAAK,OAAO,MAAM,gBAChB,KAAK,IACL,MAAM,GACN,MAAM,GACN,WACD;;IAEH;;;;;;;CAQN,IAAI,QAAQ;AACV,SAAO,OAAc,aAAa;;;;;;;CAQpC,IAAI,SAAS;AACX,SAAO,OAAO,gBAAgB;;CAMhC,yBAAiC;AAC/B,MAAI,KAAK,uBAAuB;AAC9B,QAAK,sBAAsB,aAAa;AACxC,QAAK,wBAAwB,KAAA;;AAE/B,MAAI,KAAK,uBAAuB;AAC9B,gBAAa,KAAK,sBAAsB;AACxC,QAAK,wBAAwB,KAAA;;;CAIjC,2BAAmC;EACjC,MAAM,eAAe,KAAK;AAC1B,OAAK,wBAAwB;AAC7B,OAAK,sBAAsB,IAAI,EAAE;AACjC,MAAI,cAAc;AAChB,QAAK,cAAc,IAAI,aAAa,cAAc;AAClD,QAAK,SAAS,IAAI,CAAC,GAAG,aAAa,SAAS,CAAC;;AAE/C,OAAK,wBAAwB,KAAA;AAC7B,OAAK,mBAAmB,IAAI,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDpC,MAAM,SAMG;EACP,MAAM,eAAe;GACnB,MAAM,SAAS,QAAQ;GACvB,UAAU,SAAS,YAAY;GAC/B,QAAQ,SAAS,UAAU;GAC3B,OAAO,SAAS,SAAS;GACzB,MAAM,SAAS,QAAQ;GACxB;EAGD,IAAI,YAAY,aAAa;AAC7B,MAAI,OAAO,cAAc,SAYvB,aAAY;GATV,SAAS;GACT,OAAO;GACP,SAAS;GACT,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,WAAW;GACX,SAAS;GAEC,CAAS,UAAU,aAAa,KAAK;AAGnD,OAAK,aAAa,MAAM;GACtB,GAAG;GACH,MAAM;GACP,CAAC;;;;;;;;;;;;;;CAeJ,sBAAsB;AACpB,MAAI,KAAK,uBAAuB;AAC9B,QAAK,0BAA0B;AAC/B;;AAEF,OAAK,mBAAmB,IAAI,MAAM;AAClC,OAAK,sBAAsB,IAAI,EAAE;AACjC,OAAK,wBAAwB;;CAyC/B,aACE,eACA,kBACA,kBACA,SACM;EACN,IAAI;EACJ,IAAI,eAAuB;EAC3B,IAAI,iBAAsD;AAG1D,MAAI,OAAO,qBAAqB,UAAU;AAExC,kBAAe;AACf,oBAAiB,OAAO,qBAAqB,WAAW,mBAAmB;aAClE,qBAAqB,KAAA,GAAW;AAEzC,aAAU;AACV,OAAI,OAAO,qBAAqB,SAC9B,gBAAe;QACV;AACL,mBAAe;AACf,qBAAiB,oBAAoB;;QAIvC,gBAAe;AAGjB,MAAI,KAAK,oBAAoB,CAC3B,MAAK,0BAA0B;AAGjC,OAAK,mBAAmB,IAAI,KAAK;EACjC,MAAM,wBACJ,gBAAgB,wBAAwB,KAAK,eAAe;EAC9D,MAAM,mBAAmB,gBAAgB,kBACrC,CAAC,GAAG,eAAe,gBAAgB,GACnC,CAAC,GAAG,KAAK,UAAU,CAAC;AACxB,OAAK,wBAAwB;GAC3B,eAAe;GACf,UAAU;GACX;AACD,OAAK,sBAAsB,IAAI,EAAE;AAGjC,MAAI,YAAY,KAAA,EACd,KAAI,MAAM,QAAQ,QAAQ,CACxB,MAAK,SAAS,IAAI,QAAQ;MAE1B,MAAK,SAAS,IAAI,CAAC,QAAQ,CAAC;AAIhC,OAAK,wBAAwB;AAE7B,OAAK,wBACH,KAAK,sBAAsB,WAAW,WAAW,UAAU;AACzD,OAAI,SAAS,aACX,MAAK,0BAA0B;IAEjC;AAEJ,MAAI,iBAAiB,SACnB,MAAK,wBAAwB,iBAAiB;AAC5C,OAAI,KAAK,oBAAoB,CAC3B,MAAK,0BAA0B;KAEhC,KAAK,IAAI,KAAM,eAAe,IAAK,CAAC;AAGzC,OAAK,cAAc,IAAI,cAAc;;;;;;;;CASvC,uBAAuB,IAAY,QAAa;AAC/B,SAAO,gBACtB,CAAO,sBAAsB,GAAG,CAAC,cAAc,QAAQ,KAAK;;;;;;;CAQ9D,UAAmB;AACjB,SAAO,KAAK,UAAU;;;;;;;CAQxB,WAAoB;AAClB,SAAO,KAAK,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"Gui.js","names":[],"sources":["../../src/Gui/Gui.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport { signal, Signal, WritableSignal } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"../services/AbstractSocket\";\nimport { DialogboxComponent, ShopComponent, SaveLoadComponent, MainMenuComponent, NotificationComponent, TitleScreenComponent, GameoverComponent } from \"../components/gui\";\nimport { combineLatest, Subscription } from \"rxjs\";\nimport { delay, PrebuiltGui } from \"@rpgjs/common\";\n\ninterface GuiOptions {\n name?: string;\n id?: string;\n component: any;\n display?: boolean;\n data?: any;\n /**\n * Auto display the GUI when added to the system\n * @default false\n */\n autoDisplay?: boolean;\n /**\n * Function that returns an array of Signal dependencies\n * The GUI will only display when all dependencies are resolved (!= undefined)\n * @returns Array of Signal dependencies\n */\n dependencies?: () => Signal[];\n /**\n * Attach the GUI to sprites instead of displaying globally\n * When true, the GUI will be rendered in character.ce for each sprite\n * @default false\n */\n attachToSprite?: boolean;\n}\n\ninterface GuiInstance {\n name: string;\n component: any;\n display: WritableSignal<boolean>;\n data: WritableSignal<any>;\n autoDisplay: boolean;\n dependencies?: () => Signal[];\n subscription?: Subscription;\n attachToSprite?: boolean;\n}\n\ninterface GuiAction {\n guiId: string;\n name: string;\n data: any;\n clientActionId: string;\n}\n\ntype OptimisticReducer = (data: any, action: GuiAction) => any;\n\nconst throwError = (id: string) => {\n throw `The GUI named ${id} is non-existent. Please add the component in the gui property of the decorator @RpgClient`;\n};\n\nconst updateItemQuantity = (items: any[], id: string) => {\n const index = items.findIndex((item) => item?.id === id);\n if (index === -1) return items;\n const item = items[index];\n if (item?.usable === false) return items;\n if (item?.consumable === false) return items;\n const quantity = typeof item?.quantity === \"number\" ? item.quantity : 1;\n const nextQuantity = Math.max(0, quantity - 1);\n if (nextQuantity === quantity) return items;\n if (nextQuantity <= 0) {\n return items.filter((_, idx) => idx !== index);\n }\n const nextItems = items.slice();\n nextItems[index] = { ...item, quantity: nextQuantity };\n return nextItems;\n};\n\nconst updateEquippedFlag = (items: any[], id: string, equip: boolean) => {\n const index = items.findIndex((item) => item?.id === id);\n if (index === -1) return items;\n const item = items[index];\n if (item?.equipped === equip) return items;\n const nextItems = items.slice();\n nextItems[index] = { ...item, equipped: equip };\n return nextItems;\n};\n\nconst mainMenuOptimisticReducer: OptimisticReducer = (data, action) => {\n if (!data || typeof data !== \"object\") return data;\n if (action.name === \"useItem\") {\n if (!Array.isArray(data.items)) return data;\n const id = action.data?.id;\n if (!id) return data;\n const nextItems = updateItemQuantity(data.items, id);\n if (nextItems === data.items) return data;\n return { ...data, items: nextItems };\n }\n if (action.name === \"equipItem\") {\n const id = action.data?.id;\n if (!id || typeof action.data?.equip !== \"boolean\") return data;\n const equip = action.data.equip;\n let nextItems = data.items;\n let nextEquips = data.equips;\n if (Array.isArray(data.items)) {\n nextItems = updateEquippedFlag(data.items, id, equip);\n }\n if (Array.isArray(data.equips)) {\n nextEquips = updateEquippedFlag(data.equips, id, equip);\n }\n if (nextItems === data.items && nextEquips === data.equips) return data;\n return {\n ...data,\n ...(nextItems !== data.items ? { items: nextItems } : {}),\n ...(nextEquips !== data.equips ? { equips: nextEquips } : {})\n };\n }\n return data;\n};\n\nexport class RpgGui {\n private webSocket: AbstractWebsocket;\n gui = signal<Record<string, GuiInstance>>({});\n extraGuis: GuiInstance[] = [];\n private vueGuiInstance: any = null; // Reference to VueGui instance\n private optimisticReducers = new Map<string, OptimisticReducer[]>();\n private pendingActions = new Map<string, GuiAction[]>();\n /**\n * Signal tracking which player IDs should display attached GUIs\n * Key: player ID, Value: boolean (true = show, false = hide)\n */\n attachedGuiDisplayState = signal<Record<string, boolean>>({});\n\n constructor(private context: Context) {\n this.webSocket = inject(context, WebSocketToken);\n this.add({\n name: \"rpg-dialog\",\n component: DialogboxComponent,\n });\n this.add({\n name: PrebuiltGui.MainMenu,\n component: MainMenuComponent,\n });\n this.add({\n name: PrebuiltGui.Shop,\n component: ShopComponent,\n });\n this.add({\n name: PrebuiltGui.Notification,\n component: NotificationComponent,\n autoDisplay: true,\n });\n this.add({\n name: PrebuiltGui.Save,\n component: SaveLoadComponent,\n });\n this.add({\n name: PrebuiltGui.TitleScreen,\n component: TitleScreenComponent,\n });\n this.add({\n name: PrebuiltGui.Gameover,\n component: GameoverComponent,\n });\n\n this.registerOptimisticReducer(PrebuiltGui.MainMenu, mainMenuOptimisticReducer);\n }\n\n async _initialize() {\n this.webSocket.on(\"gui.open\", (data: { guiId: string; data: any }) => {\n this.clearPendingActions(data.guiId);\n this.display(data.guiId, data.data);\n });\n\n this.webSocket.on(\"gui.exit\", (guiId: string) => {\n this.hide(guiId);\n });\n\n this.webSocket.on(\"gui.update\", (payload: { guiId: string; data: any; clientActionId?: string }) => {\n this.applyServerUpdate(payload.guiId, payload.data, payload.clientActionId);\n });\n\n /**\n * Listen for tooltip display state changes from server\n * This is triggered by showAttachedGui/hideAttachedGui on the server\n */\n this.webSocket.on(\"gui.tooltip\", (data: { players: string[]; display: boolean }) => {\n const currentState = { ...this.attachedGuiDisplayState() };\n data.players.forEach((playerId) => {\n currentState[playerId] = data.display;\n });\n this.attachedGuiDisplayState.set(currentState);\n });\n }\n\n /**\n * Set the VueGui instance reference for Vue component management\n * This is called by VueGui when it's initialized\n * \n * @param vueGuiInstance - The VueGui instance\n */\n _setVueGuiInstance(vueGuiInstance: any) {\n this.vueGuiInstance = vueGuiInstance;\n }\n\n /**\n * Notify VueGui about GUI state changes\n * This synchronizes the Vue component display state\n * \n * @param guiId - The GUI component ID\n * @param display - Display state\n * @param data - Component data\n */\n private _notifyVueGui(guiId: string, display: boolean, data: any = {}) {\n if (this.vueGuiInstance && this.vueGuiInstance.vm) {\n // Find the GUI in extraGuis\n const extraGui = this.extraGuis.find(gui => gui.name === guiId);\n if (extraGui) {\n // Update the Vue component's display state and data\n this.vueGuiInstance.vm.gui[guiId] = {\n name: guiId,\n display,\n data,\n attachToSprite: extraGui.attachToSprite || false\n };\n // Trigger Vue reactivity\n this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);\n }\n }\n }\n\n /**\n * Initialize Vue components in the VueGui instance\n * This should be called after VueGui is mounted\n */\n _initializeVueComponents() {\n if (this.vueGuiInstance && this.vueGuiInstance.vm) {\n // Initialize all extraGuis in the Vue instance\n this.extraGuis.forEach(gui => {\n this.vueGuiInstance.vm.gui[gui.name] = {\n name: gui.name,\n display: gui.display(),\n data: gui.data(),\n attachToSprite: gui.attachToSprite || false\n };\n });\n \n // Trigger Vue reactivity\n this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);\n }\n }\n\n guiInteraction(guiId: string, name: string, data: any) {\n const clientActionId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;\n const actionData = { ...(data || {}), clientActionId };\n this.applyOptimisticAction({\n guiId,\n name,\n data: actionData,\n clientActionId\n });\n this.webSocket.emit(\"gui.interaction\", {\n guiId,\n name,\n data: actionData,\n });\n }\n\n guiClose(guiId: string, data?: any) {\n this.webSocket.emit(\"gui.exit\", {\n guiId,\n data,\n });\n }\n\n /**\n * Add a GUI component to the system\n * \n * By default, only CanvasEngine components (.ce files) are accepted.\n * Vue components should be handled by the @rpgjs/vue package.\n * \n * @param gui - GUI configuration options\n * @param gui.name - Name or ID of the GUI component\n * @param gui.id - Alternative ID if name is not provided\n * @param gui.component - The component to render (must be a CanvasEngine component)\n * @param gui.display - Initial display state (default: false)\n * @param gui.data - Initial data for the component\n * @param gui.autoDisplay - Auto display when added (default: false)\n * @param gui.dependencies - Function returning Signal dependencies\n * @param gui.attachToSprite - Attach GUI to sprites instead of global display (default: false)\n * \n * @example\n * ```ts\n * gui.add({\n * name: 'inventory',\n * component: InventoryComponent, // Must be a .ce component\n * autoDisplay: true,\n * dependencies: () => [playerSignal, inventorySignal]\n * });\n * \n * // Attach to sprites\n * gui.add({\n * name: 'tooltip',\n * component: TooltipComponent,\n * attachToSprite: true\n * });\n * ```\n */\n add(gui: GuiOptions) {\n const guiId = gui.name || gui.id;\n if (!guiId) {\n throw new Error(\"GUI must have a name or id\");\n }\n const guiInstance: GuiInstance = {\n name: guiId,\n component: gui.component,\n display: signal(gui.display || false),\n data: signal(gui.data || {}),\n autoDisplay: gui.autoDisplay || false,\n dependencies: gui.dependencies ? gui.dependencies() : [],\n attachToSprite: gui.attachToSprite || false,\n };\n\n // Accept both CanvasEngine components (.ce) and Vue components\n // Vue components will be handled by VueGui if available\n if (typeof gui.component !== 'function') {\n guiInstance.component = gui;\n this.extraGuis.push(guiInstance);\n \n // Auto display Vue components if enabled\n if (guiInstance.autoDisplay) {\n this._notifyVueGui(guiId, true, gui.data || {});\n }\n return;\n }\n\n this.gui()[guiId] = guiInstance;\n\n // Auto display if enabled and it's a CanvasEngine component\n if (guiInstance.autoDisplay && typeof gui.component === 'function') {\n this.display(guiId, gui.data);\n }\n }\n\n registerOptimisticReducer(guiId: string, reducer: OptimisticReducer) {\n const existing = this.optimisticReducers.get(guiId) || [];\n this.optimisticReducers.set(guiId, existing.concat(reducer));\n }\n\n /**\n * Get all attached GUI components (attachToSprite: true)\n * \n * Returns all GUI instances that are configured to be attached to sprites.\n * These GUIs should be rendered in character.ce instead of canvas.ce.\n * \n * @returns Array of GUI instances with attachToSprite: true\n * \n * @example\n * ```ts\n * const attachedGuis = gui.getAttachedGuis();\n * // Use in character.ce to render tooltips\n * ```\n */\n getAttachedGuis(): GuiInstance[] {\n const allGuis = this.getAll();\n return Object.values(allGuis).filter(gui => gui.attachToSprite === true);\n }\n\n /**\n * Check if a player should display attached GUIs\n * \n * @param playerId - The player ID to check\n * @returns true if attached GUIs should be displayed for this player\n */\n shouldDisplayAttachedGui(playerId: string): boolean {\n return this.attachedGuiDisplayState()[playerId] === true;\n }\n\n get(id: string): GuiInstance | undefined {\n // Check CanvasEngine GUIs first\n const canvasGui = this.gui()[id];\n if (canvasGui) {\n return canvasGui;\n }\n \n // Check Vue GUIs in extraGuis\n return this.extraGuis.find(gui => gui.name === id);\n }\n\n exists(id: string): boolean {\n return !!this.get(id);\n }\n\n getAll(): Record<string, GuiInstance> {\n const allGuis = { ...this.gui() };\n \n // Add extraGuis to the result\n this.extraGuis.forEach(gui => {\n allGuis[gui.name] = gui;\n });\n \n return allGuis;\n }\n\n /**\n * Display a GUI component\n * \n * Displays the GUI immediately if no dependencies are configured,\n * or waits for all dependencies to be resolved if dependencies are present.\n * Automatically manages subscriptions to prevent memory leaks.\n * Works with both CanvasEngine components and Vue components.\n * \n * @param id - The GUI component ID\n * @param data - Data to pass to the component\n * @param dependencies - Optional runtime dependencies (overrides config dependencies)\n * \n * @example\n * ```ts\n * // Display immediately\n * gui.display('inventory', { items: [] });\n * \n * // Display with runtime dependencies\n * gui.display('shop', { shopId: 1 }, [playerSignal, shopSignal]);\n * ```\n */\n display(id: string, data = {}, dependencies: Signal[] = []) {\n if (!this.exists(id)) {\n throw throwError(id);\n }\n\n const guiInstance = this.get(id)!;\n \n // Check if it's a Vue component (in extraGuis)\n const isVueComponent = this.extraGuis.some(gui => gui.name === id);\n \n if (isVueComponent) {\n // Handle Vue component display\n this._handleVueComponentDisplay(id, data, dependencies, guiInstance);\n } else {\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n }\n }\n\n isDisplaying(id: string): boolean {\n const guiInstance = this.get(id);\n if (!guiInstance) return false;\n return guiInstance.display();\n }\n\n /**\n * Handle Vue component display logic\n * \n * @param id - GUI component ID\n * @param data - Component data\n * @param dependencies - Runtime dependencies\n * @param guiInstance - GUI instance\n */\n private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {\n // Unsubscribe from previous subscription if exists\n if (guiInstance.subscription) {\n guiInstance.subscription.unsubscribe();\n guiInstance.subscription = undefined;\n }\n\n // Use runtime dependencies or config dependencies\n const deps = dependencies.length > 0 \n ? dependencies \n : (guiInstance.dependencies ? guiInstance.dependencies() : []);\n\n if (deps.length > 0) {\n // Subscribe to dependencies\n guiInstance.subscription = combineLatest(\n deps.map(dependency => dependency.observable)\n ).subscribe((values) => {\n if (values.every(value => value !== undefined)) {\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n this._notifyVueGui(id, true, data);\n }\n });\n return;\n }\n\n // No dependencies, display immediately\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n this._notifyVueGui(id, true, data);\n }\n\n /**\n * Hide a GUI component\n * \n * Hides the GUI and cleans up any active subscriptions.\n * Works with both CanvasEngine components and Vue components.\n * \n * @param id - The GUI component ID\n * \n * @example\n * ```ts\n * gui.hide('inventory');\n * ```\n */\n hide(id: string) {\n if (!this.exists(id)) {\n throw throwError(id);\n }\n\n const guiInstance = this.get(id)!;\n \n // Unsubscribe if there's an active subscription\n if (guiInstance.subscription) {\n guiInstance.subscription.unsubscribe();\n guiInstance.subscription = undefined;\n }\n\n guiInstance.display.set(false)\n \n // Check if it's a Vue component and notify VueGui\n const isVueComponent = this.extraGuis.some(gui => gui.name === id);\n if (isVueComponent) {\n this._notifyVueGui(id, false);\n }\n }\n\n private isVueComponent(id: string) {\n return this.extraGuis.some(gui => gui.name === id);\n }\n\n private clearPendingActions(guiId: string) {\n this.pendingActions.delete(guiId);\n }\n\n private applyReducers(guiId: string, data: any, actions: GuiAction[]) {\n const reducers = this.optimisticReducers.get(guiId);\n if (!reducers || reducers.length === 0) return data;\n let next = data;\n for (const action of actions) {\n for (const reducer of reducers) {\n const updated = reducer(next, action);\n if (updated !== undefined && updated !== null && updated !== next) {\n next = updated;\n }\n }\n }\n return next;\n }\n\n private applyOptimisticAction(action: GuiAction) {\n const guiInstance = this.get(action.guiId);\n if (!guiInstance) return;\n const reducers = this.optimisticReducers.get(action.guiId);\n if (!reducers || reducers.length === 0) return;\n const currentData = guiInstance.data();\n const nextData = this.applyReducers(action.guiId, currentData, [action]);\n if (nextData === currentData) return;\n guiInstance.data.set(nextData);\n const pending = this.pendingActions.get(action.guiId) || [];\n pending.push(action);\n this.pendingActions.set(action.guiId, pending);\n if (this.isVueComponent(action.guiId)) {\n this._notifyVueGui(action.guiId, guiInstance.display(), nextData);\n }\n }\n\n private applyServerUpdate(guiId: string, data: any, clientActionId?: string) {\n const guiInstance = this.get(guiId);\n if (!guiInstance) return;\n let pending = this.pendingActions.get(guiId) || [];\n if (clientActionId) {\n pending = pending.filter(action => action.clientActionId !== clientActionId);\n } else {\n pending = [];\n }\n let nextData = data;\n if (pending.length) {\n nextData = this.applyReducers(guiId, nextData, pending);\n }\n guiInstance.data.set(nextData);\n this.pendingActions.set(guiId, pending);\n if (this.isVueComponent(guiId)) {\n this._notifyVueGui(guiId, guiInstance.display(), nextData);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Gui.js","names":[],"sources":["../../src/Gui/Gui.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport { signal, Signal, WritableSignal } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"../services/AbstractSocket\";\nimport { DialogboxComponent, ShopComponent, SaveLoadComponent, MainMenuComponent, NotificationComponent, TitleScreenComponent, GameoverComponent } from \"../components/gui\";\nimport { combineLatest, Subscription } from \"rxjs\";\nimport { delay, PrebuiltGui } from \"@rpgjs/common\";\n\ninterface GuiOptions {\n name?: string;\n id?: string;\n component: any;\n display?: boolean;\n data?: any;\n /**\n * Auto display the GUI when added to the system\n * @default false\n */\n autoDisplay?: boolean;\n /**\n * Function that returns an array of Signal dependencies\n * The GUI will only display when all dependencies are resolved (!= undefined)\n * @returns Array of Signal dependencies\n */\n dependencies?: () => Signal[];\n /**\n * Attach the GUI to sprites instead of displaying globally\n * When true, the GUI will be rendered in character.ce for each sprite\n * @default false\n */\n attachToSprite?: boolean;\n}\n\ninterface GuiInstance {\n name: string;\n component: any;\n display: WritableSignal<boolean>;\n data: WritableSignal<any>;\n autoDisplay: boolean;\n dependencies?: () => Signal[];\n subscription?: Subscription;\n attachToSprite?: boolean;\n}\n\ninterface GuiAction {\n guiId: string;\n name: string;\n data: any;\n clientActionId: string;\n}\n\ntype OptimisticReducer = (data: any, action: GuiAction) => any;\n\nconst throwError = (id: string) => {\n throw `The GUI named ${id} is non-existent. Please add the component in the gui property of the decorator @RpgClient`;\n};\n\nconst updateItemQuantity = (items: any[], id: string) => {\n const index = items.findIndex((item) => item?.id === id);\n if (index === -1) return items;\n const item = items[index];\n if (item?.usable === false) return items;\n if (item?.consumable === false) return items;\n const quantity = typeof item?.quantity === \"number\" ? item.quantity : 1;\n const nextQuantity = Math.max(0, quantity - 1);\n if (nextQuantity === quantity) return items;\n if (nextQuantity <= 0) {\n return items.filter((_, idx) => idx !== index);\n }\n const nextItems = items.slice();\n nextItems[index] = { ...item, quantity: nextQuantity };\n return nextItems;\n};\n\nconst updateEquippedFlag = (items: any[], id: string, equip: boolean) => {\n const index = items.findIndex((item) => item?.id === id);\n if (index === -1) return items;\n const item = items[index];\n if (item?.equipped === equip) return items;\n const nextItems = items.slice();\n nextItems[index] = { ...item, equipped: equip };\n return nextItems;\n};\n\nconst mainMenuOptimisticReducer: OptimisticReducer = (data, action) => {\n if (!data || typeof data !== \"object\") return data;\n if (action.name === \"useItem\") {\n if (!Array.isArray(data.items)) return data;\n const id = action.data?.id;\n if (!id) return data;\n const nextItems = updateItemQuantity(data.items, id);\n if (nextItems === data.items) return data;\n return { ...data, items: nextItems };\n }\n if (action.name === \"equipItem\") {\n const id = action.data?.id;\n if (!id || typeof action.data?.equip !== \"boolean\") return data;\n const equip = action.data.equip;\n let nextItems = data.items;\n let nextEquips = data.equips;\n if (Array.isArray(data.items)) {\n nextItems = updateEquippedFlag(data.items, id, equip);\n }\n if (Array.isArray(data.equips)) {\n nextEquips = updateEquippedFlag(data.equips, id, equip);\n }\n if (nextItems === data.items && nextEquips === data.equips) return data;\n return {\n ...data,\n ...(nextItems !== data.items ? { items: nextItems } : {}),\n ...(nextEquips !== data.equips ? { equips: nextEquips } : {})\n };\n }\n return data;\n};\n\nexport class RpgGui {\n private webSocket: AbstractWebsocket;\n gui = signal<Record<string, GuiInstance>>({});\n extraGuis: GuiInstance[] = [];\n private vueGuiInstance: any = null; // Reference to VueGui instance\n private optimisticReducers = new Map<string, OptimisticReducer[]>();\n private pendingActions = new Map<string, GuiAction[]>();\n /**\n * Signal tracking which player IDs should display attached GUIs\n * Key: player ID, Value: boolean (true = show, false = hide)\n */\n attachedGuiDisplayState = signal<Record<string, boolean>>({});\n\n constructor(private context: Context) {\n this.webSocket = inject(context, WebSocketToken);\n this.add({\n name: \"rpg-dialog\",\n component: DialogboxComponent,\n });\n this.add({\n name: PrebuiltGui.MainMenu,\n component: MainMenuComponent,\n });\n this.add({\n name: PrebuiltGui.Shop,\n component: ShopComponent,\n });\n this.add({\n name: PrebuiltGui.Notification,\n component: NotificationComponent,\n autoDisplay: true,\n });\n this.add({\n name: PrebuiltGui.Save,\n component: SaveLoadComponent,\n });\n this.add({\n name: PrebuiltGui.TitleScreen,\n component: TitleScreenComponent,\n });\n this.add({\n name: PrebuiltGui.Gameover,\n component: GameoverComponent,\n });\n\n this.registerOptimisticReducer(PrebuiltGui.MainMenu, mainMenuOptimisticReducer);\n }\n\n async _initialize() {\n this.webSocket.on(\"gui.open\", (data: { guiId: string; data: any }) => {\n this.clearPendingActions(data.guiId);\n this.display(data.guiId, data.data);\n });\n\n this.webSocket.on(\"gui.exit\", (guiId: string) => {\n this.hide(guiId);\n });\n\n this.webSocket.on(\"gui.update\", (payload: { guiId: string; data: any; clientActionId?: string }) => {\n this.applyServerUpdate(payload.guiId, payload.data, payload.clientActionId);\n });\n\n /**\n * Listen for tooltip display state changes from server\n * This is triggered by showAttachedGui/hideAttachedGui on the server\n */\n this.webSocket.on(\"gui.tooltip\", (data: { players: string[]; display: boolean }) => {\n const currentState = { ...this.attachedGuiDisplayState() };\n data.players.forEach((playerId) => {\n currentState[playerId] = data.display;\n });\n this.attachedGuiDisplayState.set(currentState);\n });\n }\n\n /**\n * Set the VueGui instance reference for Vue component management\n * This is called by VueGui when it's initialized\n * \n * @param vueGuiInstance - The VueGui instance\n */\n _setVueGuiInstance(vueGuiInstance: any) {\n this.vueGuiInstance = vueGuiInstance;\n }\n\n /**\n * Notify VueGui about GUI state changes\n * This synchronizes the Vue component display state\n * \n * @param guiId - The GUI component ID\n * @param display - Display state\n * @param data - Component data\n */\n private _notifyVueGui(guiId: string, display: boolean, data: any = {}) {\n if (this.vueGuiInstance && this.vueGuiInstance.vm) {\n // Find the GUI in extraGuis\n const extraGui = this.extraGuis.find(gui => gui.name === guiId);\n if (extraGui) {\n // Update the Vue component's display state and data\n this.vueGuiInstance.vm.gui[guiId] = {\n name: guiId,\n display,\n data,\n attachToSprite: extraGui.attachToSprite || false\n };\n // Trigger Vue reactivity\n this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);\n }\n }\n }\n\n /**\n * Initialize Vue components in the VueGui instance\n * This should be called after VueGui is mounted\n */\n _initializeVueComponents() {\n if (this.vueGuiInstance && this.vueGuiInstance.vm) {\n // Initialize all extraGuis in the Vue instance\n this.extraGuis.forEach(gui => {\n this.vueGuiInstance.vm.gui[gui.name] = {\n name: gui.name,\n display: gui.display(),\n data: gui.data(),\n attachToSprite: gui.attachToSprite || false\n };\n });\n \n // Trigger Vue reactivity\n this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);\n }\n }\n\n guiInteraction(guiId: string, name: string, data: any) {\n const clientActionId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;\n const actionData = { ...(data || {}), clientActionId };\n this.applyOptimisticAction({\n guiId,\n name,\n data: actionData,\n clientActionId\n });\n this.webSocket.emit(\"gui.interaction\", {\n guiId,\n name,\n data: actionData,\n });\n }\n\n guiClose(guiId: string, data?: any) {\n this.webSocket.emit(\"gui.exit\", {\n guiId,\n data,\n });\n }\n\n /**\n * Add a GUI component to the system\n * \n * By default, only CanvasEngine components (.ce files) are accepted.\n * Vue components should be handled by the @rpgjs/vue package.\n * \n * @param gui - GUI configuration options\n * @param gui.name - Name or ID of the GUI component\n * @param gui.id - Alternative ID if name is not provided\n * @param gui.component - The component to render (must be a CanvasEngine component)\n * @param gui.display - Initial display state (default: false)\n * @param gui.data - Initial data for the component\n * @param gui.autoDisplay - Auto display when added (default: false)\n * @param gui.dependencies - Function returning Signal dependencies\n * @param gui.attachToSprite - Attach GUI to sprites instead of global display (default: false)\n * \n * @example\n * ```ts\n * gui.add({\n * name: 'inventory',\n * component: InventoryComponent, // Must be a .ce component\n * autoDisplay: true,\n * dependencies: () => [playerSignal, inventorySignal]\n * });\n * \n * // Attach to sprites\n * gui.add({\n * name: 'tooltip',\n * component: TooltipComponent,\n * attachToSprite: true\n * });\n * ```\n */\n add(gui: GuiOptions) {\n const guiId = gui.name || gui.id;\n if (!guiId) {\n throw new Error(\"GUI must have a name or id\");\n }\n const guiInstance: GuiInstance = {\n name: guiId,\n component: gui.component,\n display: signal(gui.display || false),\n data: signal(gui.data || {}),\n autoDisplay: gui.autoDisplay || false,\n dependencies: gui.dependencies ? gui.dependencies() : [],\n attachToSprite: gui.attachToSprite || false,\n };\n\n // Accept both CanvasEngine components (.ce) and Vue components\n // Vue components will be handled by VueGui if available\n if (typeof gui.component !== 'function') {\n guiInstance.component = gui;\n this.extraGuis.push(guiInstance);\n \n // Auto display Vue components if enabled\n if (guiInstance.autoDisplay) {\n this._notifyVueGui(guiId, true, gui.data || {});\n }\n return;\n }\n\n this.gui()[guiId] = guiInstance;\n\n // Auto display if enabled and it's a CanvasEngine component\n if (guiInstance.autoDisplay && typeof gui.component === 'function') {\n this.display(guiId, gui.data);\n }\n }\n\n registerOptimisticReducer(guiId: string, reducer: OptimisticReducer) {\n const existing = this.optimisticReducers.get(guiId) || [];\n this.optimisticReducers.set(guiId, existing.concat(reducer));\n }\n\n /**\n * Get all attached GUI components (attachToSprite: true)\n * \n * Returns all GUI instances that are configured to be attached to sprites.\n * These GUIs should be rendered in character.ce instead of canvas.ce.\n * \n * @returns Array of GUI instances with attachToSprite: true\n * \n * @example\n * ```ts\n * const attachedGuis = gui.getAttachedGuis();\n * // Use in character.ce to render tooltips\n * ```\n */\n getAttachedGuis(): GuiInstance[] {\n const allGuis = this.getAll();\n return Object.values(allGuis).filter(gui => gui.attachToSprite === true);\n }\n\n /**\n * Check if a player should display attached GUIs\n * \n * @param playerId - The player ID to check\n * @returns true if attached GUIs should be displayed for this player\n */\n shouldDisplayAttachedGui(playerId: string): boolean {\n return this.attachedGuiDisplayState()[playerId] === true;\n }\n\n get(id: string): GuiInstance | undefined {\n // Check CanvasEngine GUIs first\n const canvasGui = this.gui()[id];\n if (canvasGui) {\n return canvasGui;\n }\n \n // Check Vue GUIs in extraGuis\n return this.extraGuis.find(gui => gui.name === id);\n }\n\n exists(id: string): boolean {\n return !!this.get(id);\n }\n\n getAll(): Record<string, GuiInstance> {\n const allGuis = { ...this.gui() };\n \n // Add extraGuis to the result\n this.extraGuis.forEach(gui => {\n allGuis[gui.name] = gui;\n });\n \n return allGuis;\n }\n\n /**\n * Display a GUI component\n * \n * Displays the GUI immediately if no dependencies are configured,\n * or waits for all dependencies to be resolved if dependencies are present.\n * Automatically manages subscriptions to prevent memory leaks.\n * Works with both CanvasEngine components and Vue components.\n * \n * @param id - The GUI component ID\n * @param data - Data to pass to the component\n * @param dependencies - Optional runtime dependencies (overrides config dependencies)\n * \n * @example\n * ```ts\n * // Display immediately\n * gui.display('inventory', { items: [] });\n * \n * // Display with runtime dependencies\n * gui.display('shop', { shopId: 1 }, [playerSignal, shopSignal]);\n * ```\n */\n display(id: string, data = {}, dependencies: Signal[] = []) {\n if (!this.exists(id)) {\n throw throwError(id);\n }\n\n const guiInstance = this.get(id)!;\n \n // Check if it's a Vue component (in extraGuis)\n const isVueComponent = this.extraGuis.some(gui => gui.name === id);\n \n if (isVueComponent) {\n // Handle Vue component display\n this._handleVueComponentDisplay(id, data, dependencies, guiInstance);\n } else {\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n }\n }\n\n isDisplaying(id: string): boolean {\n const guiInstance = this.get(id);\n if (!guiInstance) return false;\n return guiInstance.display();\n }\n\n /**\n * Handle Vue component display logic\n * \n * @param id - GUI component ID\n * @param data - Component data\n * @param dependencies - Runtime dependencies\n * @param guiInstance - GUI instance\n */\n private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {\n // Unsubscribe from previous subscription if exists\n if (guiInstance.subscription) {\n guiInstance.subscription.unsubscribe();\n guiInstance.subscription = undefined;\n }\n\n // Use runtime dependencies or config dependencies\n const deps = dependencies.length > 0 \n ? dependencies \n : (guiInstance.dependencies ? guiInstance.dependencies() : []);\n\n if (deps.length > 0) {\n // Subscribe to dependencies\n guiInstance.subscription = combineLatest(\n deps.map(dependency => dependency.observable)\n ).subscribe((values) => {\n if (values.every(value => value !== undefined)) {\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n this._notifyVueGui(id, true, data);\n }\n });\n return;\n }\n\n // No dependencies, display immediately\n guiInstance.data.set(data);\n guiInstance.display.set(true);\n this._notifyVueGui(id, true, data);\n }\n\n /**\n * Hide a GUI component\n * \n * Hides the GUI and cleans up any active subscriptions.\n * Works with both CanvasEngine components and Vue components.\n * \n * @param id - The GUI component ID\n * \n * @example\n * ```ts\n * gui.hide('inventory');\n * ```\n */\n hide(id: string) {\n if (!this.exists(id)) {\n throw throwError(id);\n }\n\n const guiInstance = this.get(id)!;\n \n // Unsubscribe if there's an active subscription\n if (guiInstance.subscription) {\n guiInstance.subscription.unsubscribe();\n guiInstance.subscription = undefined;\n }\n\n guiInstance.display.set(false)\n \n // Check if it's a Vue component and notify VueGui\n const isVueComponent = this.extraGuis.some(gui => gui.name === id);\n if (isVueComponent) {\n this._notifyVueGui(id, false);\n }\n }\n\n private isVueComponent(id: string) {\n return this.extraGuis.some(gui => gui.name === id);\n }\n\n private clearPendingActions(guiId: string) {\n this.pendingActions.delete(guiId);\n }\n\n private applyReducers(guiId: string, data: any, actions: GuiAction[]) {\n const reducers = this.optimisticReducers.get(guiId);\n if (!reducers || reducers.length === 0) return data;\n let next = data;\n for (const action of actions) {\n for (const reducer of reducers) {\n const updated = reducer(next, action);\n if (updated !== undefined && updated !== null && updated !== next) {\n next = updated;\n }\n }\n }\n return next;\n }\n\n private applyOptimisticAction(action: GuiAction) {\n const guiInstance = this.get(action.guiId);\n if (!guiInstance) return;\n const reducers = this.optimisticReducers.get(action.guiId);\n if (!reducers || reducers.length === 0) return;\n const currentData = guiInstance.data();\n const nextData = this.applyReducers(action.guiId, currentData, [action]);\n if (nextData === currentData) return;\n guiInstance.data.set(nextData);\n const pending = this.pendingActions.get(action.guiId) || [];\n pending.push(action);\n this.pendingActions.set(action.guiId, pending);\n if (this.isVueComponent(action.guiId)) {\n this._notifyVueGui(action.guiId, guiInstance.display(), nextData);\n }\n }\n\n private applyServerUpdate(guiId: string, data: any, clientActionId?: string) {\n const guiInstance = this.get(guiId);\n if (!guiInstance) return;\n let pending = this.pendingActions.get(guiId) || [];\n if (clientActionId) {\n pending = pending.filter(action => action.clientActionId !== clientActionId);\n } else {\n pending = [];\n }\n let nextData = data;\n if (pending.length) {\n nextData = this.applyReducers(guiId, nextData, pending);\n }\n guiInstance.data.set(nextData);\n this.pendingActions.set(guiId, pending);\n if (this.isVueComponent(guiId)) {\n this._notifyVueGui(guiId, guiInstance.display(), nextData);\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationManager.js","names":[],"sources":["../../src/Gui/NotificationManager.ts"],"sourcesContent":["import { signal, animatedSignal } from \"canvasengine\";\n\nexport type NotificationType = \"info\" | \"warn\" | \"error\";\n\nexport interface NotificationPayload {\n message: string;\n type?: NotificationType;\n icon?: string;\n time?: number;\n sound?: string;\n}\n\nexport interface NotificationItem extends NotificationPayload {\n id: number;\n opacity: any;\n offset: any;\n layoutY: any;\n removing: boolean;\n}\n\nconst DEFAULT_DURATION = 220;\n\nexport class NotificationManager {\n stack = signal<NotificationItem[]>([]);\n private _counter = 0;\n\n add(payload: NotificationPayload, engine?: { playSound?: (id: string) => void }) {\n const id = ++this._counter;\n const opacity = animatedSignal(0, { duration: DEFAULT_DURATION });\n const offset = animatedSignal(12, { duration: DEFAULT_DURATION });\n const layoutY = animatedSignal(0, { duration: DEFAULT_DURATION });\n const item: NotificationItem = {\n id,\n message: payload.message,\n type: payload.type || \"info\",\n icon: payload.icon,\n time: payload.time,\n sound: payload.sound,\n opacity,\n offset,\n layoutY,\n removing: false,\n };\n this.stack.update((list) => [...list, item]);\n opacity.set(1);\n offset.set(0);\n\n if (payload.sound && engine?.playSound) {\n engine.playSound(payload.sound);\n }\n\n const delay = typeof payload.time === \"number\" ? payload.time : 2000;\n setTimeout(() => {\n this.remove(id);\n }, delay);\n }\n\n remove(id: number) {\n const list = this.stack();\n const item = list.find((it) => it.id === id);\n if (!item || item.removing) return;\n item.removing = true;\n item.opacity.set(0);\n item.offset.set(-8);\n setTimeout(() => {\n this.stack.update((items) => items.filter((it) => it.id !== id));\n }, DEFAULT_DURATION);\n }\n}\n"],"mappings":";;AAoBA,IAAM,mBAAmB;AAEzB,IAAa,sBAAb,MAAiC;;eACvB,OAA2B,EAAE,CAAC;kBACnB;;CAEnB,IAAI,SAA8B,QAA+C;EAC/E,MAAM,KAAK,EAAE,KAAK;EAClB,MAAM,UAAU,eAAe,GAAG,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,SAAS,eAAe,IAAI,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,UAAU,eAAe,GAAG,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,OAAyB;GAC7B;GACA,SAAS,QAAQ;GACjB,MAAM,QAAQ,QAAQ;GACtB,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,OAAO,QAAQ;GACf;GACA;GACA;GACA,UAAU;GACX;AACD,OAAK,MAAM,QAAQ,SAAS,CAAC,GAAG,MAAM,KAAK,CAAC;AAC5C,UAAQ,IAAI,EAAE;AACd,SAAO,IAAI,EAAE;AAEb,MAAI,QAAQ,SAAS,QAAQ,UAC3B,QAAO,UAAU,QAAQ,MAAM;EAGjC,MAAM,QAAQ,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAChE,mBAAiB;AACf,QAAK,OAAO,GAAG;KACd,MAAM;;CAGX,OAAO,IAAY;EAEjB,MAAM,OADO,KAAK,OAAO,CACP,MAAM,OAAO,GAAG,OAAO,GAAG;AAC5C,MAAI,CAAC,QAAQ,KAAK,SAAU;AAC5B,OAAK,WAAW;AAChB,OAAK,QAAQ,IAAI,EAAE;AACnB,OAAK,OAAO,IAAI,GAAG;AACnB,mBAAiB;AACf,QAAK,MAAM,QAAQ,UAAU,MAAM,QAAQ,OAAO,GAAG,OAAO,GAAG,CAAC;KAC/D,iBAAiB"}
1
+ {"version":3,"file":"NotificationManager.js","names":[],"sources":["../../src/Gui/NotificationManager.ts"],"sourcesContent":["import { signal, animatedSignal } from \"canvasengine\";\n\nexport type NotificationType = \"info\" | \"warn\" | \"error\";\n\nexport interface NotificationPayload {\n message: string;\n type?: NotificationType;\n icon?: string;\n time?: number;\n sound?: string;\n}\n\nexport interface NotificationItem extends NotificationPayload {\n id: number;\n opacity: any;\n offset: any;\n layoutY: any;\n removing: boolean;\n}\n\nconst DEFAULT_DURATION = 220;\n\nexport class NotificationManager {\n stack = signal<NotificationItem[]>([]);\n private _counter = 0;\n\n add(payload: NotificationPayload, engine?: { playSound?: (id: string) => void }) {\n const id = ++this._counter;\n const opacity = animatedSignal(0, { duration: DEFAULT_DURATION });\n const offset = animatedSignal(12, { duration: DEFAULT_DURATION });\n const layoutY = animatedSignal(0, { duration: DEFAULT_DURATION });\n const item: NotificationItem = {\n id,\n message: payload.message,\n type: payload.type || \"info\",\n icon: payload.icon,\n time: payload.time,\n sound: payload.sound,\n opacity,\n offset,\n layoutY,\n removing: false,\n };\n this.stack.update((list) => [...list, item]);\n opacity.set(1);\n offset.set(0);\n\n if (payload.sound && engine?.playSound) {\n engine.playSound(payload.sound);\n }\n\n const delay = typeof payload.time === \"number\" ? payload.time : 2000;\n setTimeout(() => {\n this.remove(id);\n }, delay);\n }\n\n remove(id: number) {\n const list = this.stack();\n const item = list.find((it) => it.id === id);\n if (!item || item.removing) return;\n item.removing = true;\n item.opacity.set(0);\n item.offset.set(-8);\n setTimeout(() => {\n this.stack.update((items) => items.filter((it) => it.id !== id));\n }, DEFAULT_DURATION);\n }\n}\n"],"mappings":";;AAoBA,IAAM,mBAAmB;AAEzB,IAAa,sBAAb,MAAiC;;eACvB,OAA2B,EAAE,CAAC;kBACnB;;CAEnB,IAAI,SAA8B,QAA+C;EAC/E,MAAM,KAAK,EAAE,KAAK;EAClB,MAAM,UAAU,eAAe,GAAG,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,SAAS,eAAe,IAAI,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,UAAU,eAAe,GAAG,EAAE,UAAU,kBAAkB,CAAC;EACjE,MAAM,OAAyB;GAC7B;GACA,SAAS,QAAQ;GACjB,MAAM,QAAQ,QAAQ;GACtB,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,OAAO,QAAQ;GACf;GACA;GACA;GACA,UAAU;GACX;AACD,OAAK,MAAM,QAAQ,SAAS,CAAC,GAAG,MAAM,KAAK,CAAC;AAC5C,UAAQ,IAAI,EAAE;AACd,SAAO,IAAI,EAAE;AAEb,MAAI,QAAQ,SAAS,QAAQ,UAC3B,QAAO,UAAU,QAAQ,MAAM;EAGjC,MAAM,QAAQ,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAChE,mBAAiB;AACf,QAAK,OAAO,GAAG;KACd,MAAM;;CAGX,OAAO,IAAY;EAEjB,MAAM,OADO,KAAK,OACL,CAAK,MAAM,OAAO,GAAG,OAAO,GAAG;AAC5C,MAAI,CAAC,QAAQ,KAAK,SAAU;AAC5B,OAAK,WAAW;AAChB,OAAK,QAAQ,IAAI,EAAE;AACnB,OAAK,OAAO,IAAI,GAAG;AACnB,mBAAiB;AACf,QAAK,MAAM,QAAQ,UAAU,MAAM,QAAQ,OAAO,GAAG,OAAO,GAAG,CAAC;KAC/D,iBAAiB"}
@@ -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
  *
@@ -227,7 +227,8 @@ var RpgClientEngine = class {
227
227
  if ((payload.events || this.sceneMap.events()) !== void 0) this.eventsReceived$.next(true);
228
228
  });
229
229
  this.webSocket.on("pong", (data) => {
230
- this.rtt = Date.now() - data.clientTime;
230
+ const now = Date.now();
231
+ this.rtt = now - data.clientTime;
231
232
  const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1e3 / 60));
232
233
  const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
233
234
  if (this.inputFrameCounter > 0) this.frameOffset = estimatedServerTickNow - data.clientFrame;
@@ -249,10 +250,15 @@ var RpgClientEngine = class {
249
250
  this.notificationManager.add(data);
250
251
  });
251
252
  this.webSocket.on("setAnimation", (data) => {
252
- const { animationName, nbTimes, object, graphic } = data;
253
- const player = this.sceneMap.getObjectById(object);
254
- if (graphic !== void 0) player.setAnimation(animationName, graphic, nbTimes);
255
- 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);
256
262
  });
257
263
  this.webSocket.on("playSound", (data) => {
258
264
  const { soundId, volume, loop } = data;
@@ -889,6 +895,12 @@ var RpgClientEngine = class {
889
895
  this.guiService.display(id, props);
890
896
  }
891
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
+ }
892
904
  const timestamp = Date.now();
893
905
  let frame;
894
906
  let tick;
@@ -905,7 +917,6 @@ var RpgClientEngine = class {
905
917
  input,
906
918
  playerId: this.playerId
907
919
  }).subscribe();
908
- const currentPlayer = this.sceneMap.getCurrentPlayer();
909
920
  const bodyReady = this.ensureCurrentPlayerBody();
910
921
  if (currentPlayer && bodyReady) {
911
922
  currentPlayer.changeDirection(input);
@@ -920,6 +931,8 @@ var RpgClientEngine = class {
920
931
  }
921
932
  processAction({ action }) {
922
933
  if (this.stopProcessingInput) return;
934
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
935
+ if (!(!currentPlayer || typeof currentPlayer.canMove !== "function" || currentPlayer.canMove())) return;
923
936
  this.hooks.callHooks("client-engine-onInput", this, {
924
937
  input: "action",
925
938
  playerId: this.playerId
@@ -1008,6 +1021,11 @@ var RpgClientEngine = class {
1008
1021
  }
1009
1022
  flushPendingMovePath() {
1010
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
+ }
1011
1029
  const pendingInputs = this.prediction.getPendingInputs();
1012
1030
  if (pendingInputs.length === 0) return;
1013
1031
  const latest = pendingInputs[pendingInputs.length - 1];
@@ -1124,6 +1142,31 @@ var RpgClientEngine = class {
1124
1142
  this.lastMovePathSentFrame = 0;
1125
1143
  }
1126
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
+ /**
1127
1170
  * Trigger a flash animation on a sprite
1128
1171
  *
1129
1172
  * This method allows you to trigger a flash effect on any sprite from client-side code.
@@ -1205,6 +1248,10 @@ var RpgClientEngine = class {
1205
1248
  reconcilePrediction(authoritativeState, pendingInputs) {
1206
1249
  const player = this.getCurrentPlayer();
1207
1250
  if (!player) return;
1251
+ if (typeof player.canMove === "function" && !player.canMove()) {
1252
+ this.interruptCurrentPlayerMovement(player);
1253
+ return;
1254
+ }
1208
1255
  this.sceneMap.stopMovement(player);
1209
1256
  this.applyAuthoritativeState(authoritativeState);
1210
1257
  if (!pendingInputs.length) return;