@rpgjs/client 5.0.0-beta.6 → 5.0.0-beta.8

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 (147) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/Game/AnimationManager.d.ts +2 -2
  3. package/dist/Game/AnimationManager.js +18 -9
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  6. package/dist/Game/Map.d.ts +7 -9
  7. package/dist/Game/Map.js +5 -4
  8. package/dist/Game/Map.js.map +1 -1
  9. package/dist/Game/Object.d.ts +44 -20
  10. package/dist/Game/Object.js +28 -14
  11. package/dist/Game/Object.js.map +1 -1
  12. package/dist/Gui/Gui.d.ts +19 -6
  13. package/dist/Gui/Gui.js +64 -34
  14. package/dist/Gui/Gui.js.map +1 -1
  15. package/dist/Gui/Gui.spec.d.ts +1 -0
  16. package/dist/Gui/NotificationManager.d.ts +1 -1
  17. package/dist/Gui/NotificationManager.js.map +1 -1
  18. package/dist/Resource.js +1 -1
  19. package/dist/Resource.js.map +1 -1
  20. package/dist/RpgClient.d.ts +57 -2
  21. package/dist/RpgClientEngine.d.ts +55 -16
  22. package/dist/RpgClientEngine.js +60 -5
  23. package/dist/RpgClientEngine.js.map +1 -1
  24. package/dist/Sound.js.map +1 -1
  25. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorate.js +1 -1
  26. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorateMetadata.js +1 -1
  27. package/dist/components/animations/animation.ce.js.map +1 -1
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/character.ce.js +280 -3
  30. package/dist/components/character.ce.js.map +1 -1
  31. package/dist/components/dynamics/bar.ce.js +96 -0
  32. package/dist/components/dynamics/bar.ce.js.map +1 -0
  33. package/dist/components/dynamics/image.ce.js +23 -0
  34. package/dist/components/dynamics/image.ce.js.map +1 -0
  35. package/dist/components/dynamics/parse-value.d.ts +4 -1
  36. package/dist/components/dynamics/parse-value.js +51 -35
  37. package/dist/components/dynamics/parse-value.js.map +1 -1
  38. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  39. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  40. package/dist/components/dynamics/shape-utils.js +73 -0
  41. package/dist/components/dynamics/shape-utils.js.map +1 -0
  42. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  43. package/dist/components/dynamics/shape.ce.js +83 -0
  44. package/dist/components/dynamics/shape.ce.js.map +1 -0
  45. package/dist/components/dynamics/text.ce.js +28 -41
  46. package/dist/components/dynamics/text.ce.js.map +1 -1
  47. package/dist/components/gui/box.ce.js.map +1 -1
  48. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  49. package/dist/components/gui/gameover.ce.js.map +1 -1
  50. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  51. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  52. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  54. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  57. package/dist/components/gui/mobile/index.d.ts +1 -1
  58. package/dist/components/gui/mobile/index.js.map +1 -1
  59. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  60. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  61. package/dist/components/gui/save-load.ce.js.map +1 -1
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js.map +1 -1
  64. package/dist/components/player-components-utils.d.ts +67 -0
  65. package/dist/components/player-components-utils.js +162 -0
  66. package/dist/components/player-components-utils.js.map +1 -0
  67. package/dist/components/player-components-utils.spec.d.ts +1 -0
  68. package/dist/components/player-components.ce.js +188 -0
  69. package/dist/components/player-components.ce.js.map +1 -0
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  72. package/dist/components/scenes/canvas.ce.js.map +1 -1
  73. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  74. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  75. package/dist/core/inject.js +1 -1
  76. package/dist/core/inject.js.map +1 -1
  77. package/dist/core/setup.js +1 -1
  78. package/dist/core/setup.js.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/module.js +4 -1
  81. package/dist/module.js.map +1 -1
  82. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@2.10.0}/node_modules/@signe/di/dist/index.js +7 -117
  83. package/dist/node_modules/.pnpm/@signe_di@2.10.0/node_modules/@signe/di/dist/index.js.map +1 -0
  84. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js +239 -0
  85. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  86. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js +611 -0
  87. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js.map +1 -0
  88. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js +44 -0
  89. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  90. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@2.10.0}/node_modules/@signe/sync/dist/index.js +29 -136
  91. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/index.js.map +1 -0
  92. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  93. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  94. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  95. package/dist/presets/animation.js.map +1 -1
  96. package/dist/presets/faceset.js.map +1 -1
  97. package/dist/presets/icon.js.map +1 -1
  98. package/dist/presets/lpc.js.map +1 -1
  99. package/dist/presets/rmspritesheet.js.map +1 -1
  100. package/dist/services/AbstractSocket.js.map +1 -1
  101. package/dist/services/keyboardControls.js.map +1 -1
  102. package/dist/services/loadMap.d.ts +6 -0
  103. package/dist/services/loadMap.js +1 -1
  104. package/dist/services/loadMap.js.map +1 -1
  105. package/dist/services/mmorpg.js +1 -1
  106. package/dist/services/mmorpg.js.map +1 -1
  107. package/dist/services/save.js.map +1 -1
  108. package/dist/services/standalone.js +1 -1
  109. package/dist/services/standalone.js.map +1 -1
  110. package/dist/utils/getEntityProp.js.map +1 -1
  111. package/package.json +8 -8
  112. package/src/Game/AnimationManager.spec.ts +30 -0
  113. package/src/Game/AnimationManager.ts +22 -10
  114. package/src/Game/Map.ts +12 -2
  115. package/src/Game/Object.ts +68 -43
  116. package/src/Gui/Gui.spec.ts +273 -0
  117. package/src/Gui/Gui.ts +105 -50
  118. package/src/Resource.ts +1 -2
  119. package/src/RpgClient.ts +63 -2
  120. package/src/RpgClientEngine.ts +82 -12
  121. package/src/components/character.ce +353 -1
  122. package/src/components/dynamics/bar.ce +87 -0
  123. package/src/components/dynamics/image.ce +20 -0
  124. package/src/components/dynamics/parse-value.spec.ts +41 -0
  125. package/src/components/dynamics/parse-value.ts +102 -37
  126. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  127. package/src/components/dynamics/shape-utils.ts +61 -0
  128. package/src/components/dynamics/shape.ce +89 -0
  129. package/src/components/dynamics/text.ce +34 -149
  130. package/src/components/player-components-utils.spec.ts +109 -0
  131. package/src/components/player-components-utils.ts +205 -0
  132. package/src/components/player-components.ce +221 -0
  133. package/src/core/setup.ts +2 -2
  134. package/src/module.ts +5 -1
  135. package/src/services/loadMap.ts +2 -0
  136. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  137. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  138. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  139. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  140. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  141. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  142. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  143. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  144. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  145. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  146. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  147. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
@@ -1,19 +1,32 @@
1
1
  import { Hooks, ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
2
- import { trigger, signal, effect } from "canvasengine";
3
- import { filter, from, map, of, Subscription, switchMap } from "rxjs";
2
+ import { trigger, signal, type Trigger } from "canvasengine";
3
+ import { from, map, of, Subscription, switchMap } from "rxjs";
4
4
  import { inject } from "../core/inject";
5
5
  import { RpgClientEngine } from "../RpgClientEngine";
6
- import TextComponent from "../components/dynamics/text.ce";
7
-
8
- const DYNAMIC_COMPONENTS = {
9
- text: TextComponent,
10
- }
11
-
12
6
  type Frame = { x: number; y: number; ts: number };
13
7
 
14
8
  type AnimationRestoreOptions = {
15
9
  restoreAnimationName?: string;
16
10
  restoreGraphics?: any[];
11
+ timeoutMs?: number;
12
+ };
13
+
14
+ type FlashType = 'alpha' | 'tint' | 'both';
15
+
16
+ type FlashOptions = {
17
+ type?: FlashType;
18
+ duration?: number;
19
+ cycles?: number;
20
+ alpha?: number;
21
+ tint?: number | string;
22
+ };
23
+
24
+ type FlashTriggerOptions = Omit<FlashOptions, "tint"> & {
25
+ tint: number;
26
+ };
27
+
28
+ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
29
+ start(config?: T): Promise<void>;
17
30
  };
18
31
 
19
32
  export abstract class RpgClientObject extends RpgCommonPlayer {
@@ -25,8 +38,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
25
38
  _param = signal({});
26
39
  frames: Frame[] = [];
27
40
  graphicsSignals = signal<any[]>([]);
28
- _component = {} // temporary component memory
29
- flashTrigger = trigger();
41
+ flashTrigger: ConfigurableTrigger<FlashTriggerOptions> = trigger<FlashTriggerOptions>();
30
42
  private animationRestoreState?: {
31
43
  animationName: string;
32
44
  graphics: any[];
@@ -57,24 +69,6 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
57
69
  this.graphicsSignals.set(sheets);
58
70
  });
59
71
 
60
- this.componentsTop.observable
61
- .pipe(
62
- filter(value => value !== null && value !== undefined),
63
- map((value) => typeof value === 'string' ? JSON.parse(value) : value),
64
- )
65
- .subscribe(({components}) => {
66
- for (const component of components) {
67
- for (const [key, value] of Object.entries(component)) {
68
- this._component = value as any; // temporary component memory
69
- console.log(value)
70
- const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;
71
- if (DYNAMIC_COMPONENTS[type]) {
72
- this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);
73
- }
74
- }
75
- }
76
- });
77
-
78
72
  this.engine.tick
79
73
  .pipe
80
74
  //throttleTime(10)
@@ -113,6 +107,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
113
107
 
114
108
  private animationSubscription?: Subscription;
115
109
  private animationResetTimeout?: ReturnType<typeof setTimeout>;
110
+ private animationWaitResolve?: () => void;
116
111
 
117
112
  private clearAnimationControls() {
118
113
  if (this.animationSubscription) {
@@ -125,6 +120,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
125
120
  }
126
121
  }
127
122
 
123
+ private resolveAnimationWait() {
124
+ const resolve = this.animationWaitResolve;
125
+ this.animationWaitResolve = undefined;
126
+ resolve?.();
127
+ }
128
+
128
129
  private finishTemporaryAnimation() {
129
130
  const restoreState = this.animationRestoreState;
130
131
  this.clearAnimationControls();
@@ -135,6 +136,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
135
136
  }
136
137
  this.animationRestoreState = undefined;
137
138
  this.animationIsPlaying.set(false);
139
+ this.resolveAnimationWait();
138
140
  }
139
141
 
140
142
  /**
@@ -183,13 +185,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
183
185
  * });
184
186
  * ```
185
187
  */
186
- flash(options?: {
187
- type?: 'alpha' | 'tint' | 'both';
188
- duration?: number;
189
- cycles?: number;
190
- alpha?: number;
191
- tint?: number | string;
192
- }): void {
188
+ flash(options?: FlashOptions): void {
193
189
  const flashOptions = {
194
190
  type: options?.type || 'alpha',
195
191
  duration: options?.duration ?? 300,
@@ -241,6 +237,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
241
237
  this.animationIsPlaying.set(false);
242
238
  this.animationCurrentIndex.set(0);
243
239
  this.clearAnimationControls();
240
+ this.resolveAnimationWait();
244
241
  }
245
242
 
246
243
  /**
@@ -252,17 +249,19 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
252
249
  *
253
250
  * @param animationName - Name of the animation to play
254
251
  * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
252
+ * @param options - Restore and timeout options
253
+ * @returns A promise resolved when a finite animation finishes, is interrupted, or times out
255
254
  *
256
255
  * @example
257
256
  * ```ts
258
257
  * // Play attack animation 3 times
259
- * player.setAnimation('attack', 3);
258
+ * await player.setAnimation('attack', 3);
260
259
  *
261
260
  * // Play continuous spell animation
262
261
  * player.setAnimation('spell');
263
262
  * ```
264
263
  */
265
- setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): void;
264
+ setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): Promise<void>;
266
265
  /**
267
266
  * Set a custom animation with temporary graphic change
268
267
  *
@@ -273,20 +272,22 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
273
272
  * @param animationName - Name of the animation to play
274
273
  * @param graphic - The graphic(s) to temporarily use during the animation
275
274
  * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
275
+ * @param options - Restore and timeout options
276
+ * @returns A promise resolved when a finite animation finishes, is interrupted, or times out
276
277
  *
277
278
  * @example
278
279
  * ```ts
279
280
  * // Play attack animation with temporary graphic change
280
- * player.setAnimation('attack', 'hero_attack', 3);
281
+ * await player.setAnimation('attack', 'hero_attack', 3);
281
282
  * ```
282
283
  */
283
- setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): void;
284
+ setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): Promise<void>;
284
285
  setAnimation(
285
286
  animationName: string,
286
287
  graphicOrNbTimes?: string | string[] | number,
287
288
  nbTimesOrOptions?: number | AnimationRestoreOptions,
288
289
  options?: AnimationRestoreOptions
289
- ): void {
290
+ ): Promise<void> {
290
291
  let graphic: string | string[] | undefined;
291
292
  let finalNbTimes: number = Infinity;
292
293
  let restoreOptions: AnimationRestoreOptions | undefined = options;
@@ -314,6 +315,13 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
314
315
  this.finishTemporaryAnimation();
315
316
  }
316
317
 
318
+ const waitPromise =
319
+ finalNbTimes === Infinity
320
+ ? Promise.resolve()
321
+ : new Promise<void>((resolve) => {
322
+ this.animationWaitResolve = resolve;
323
+ });
324
+
317
325
  this.animationIsPlaying.set(true);
318
326
  const previousAnimationName =
319
327
  restoreOptions?.restoreAnimationName ?? this.animationName();
@@ -349,10 +357,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
349
357
  if (this.animationIsPlaying()) {
350
358
  this.finishTemporaryAnimation();
351
359
  }
352
- }, Math.max(1000, finalNbTimes * 1000));
360
+ }, restoreOptions?.timeoutMs ?? Math.max(1000, finalNbTimes * 1000));
353
361
  }
354
362
 
355
363
  this.animationName.set(animationName);
364
+
365
+ return waitPromise;
356
366
  }
357
367
 
358
368
  /**
@@ -360,10 +370,25 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
360
370
  *
361
371
  * @param id - Identifier of the component animation to play.
362
372
  * @param params - Parameters forwarded to the animation effect.
373
+ * @returns A promise resolved when the animation component calls `onFinish`.
363
374
  */
364
- showComponentAnimation(id: string, params: any) {
375
+ showComponentAnimation(id: string, params: any): Promise<void> {
365
376
  const engine = inject(RpgClientEngine);
366
- engine.getComponentAnimation(id).displayEffect(params, this);
377
+ return engine.getComponentAnimation(id).displayEffect(params, this);
378
+ }
379
+
380
+ /**
381
+ * Display a registered spritesheet animation effect on this object.
382
+ *
383
+ * @param graphic - Identifier of the spritesheet to use.
384
+ * @param animationName - Name of the animation inside the spritesheet.
385
+ * @returns A promise resolved when the animation component calls `onFinish`.
386
+ */
387
+ showAnimation(graphic: string, animationName: string = 'default'): Promise<void> {
388
+ return this.showComponentAnimation('animation', {
389
+ graphic,
390
+ animationName,
391
+ });
367
392
  }
368
393
 
369
394
  /**
@@ -0,0 +1,273 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { Context, injector } from "@signe/di";
3
+ import { signal } from "canvasengine";
4
+ import { PrebuiltGui } from "@rpgjs/common";
5
+ import { WebSocketToken } from "../services/AbstractSocket";
6
+
7
+ vi.mock("../components/gui", () => {
8
+ const component = () => null;
9
+ return {
10
+ DialogboxComponent: component,
11
+ ShopComponent: component,
12
+ SaveLoadComponent: component,
13
+ MainMenuComponent: component,
14
+ NotificationComponent: component,
15
+ TitleScreenComponent: component,
16
+ GameoverComponent: component,
17
+ };
18
+ });
19
+
20
+ const createGui = async () => {
21
+ const { RpgGui } = await import("./Gui");
22
+ const context = new Context();
23
+ const socket = {
24
+ on: vi.fn(),
25
+ emit: vi.fn(),
26
+ };
27
+ await injector(context, [
28
+ {
29
+ provide: WebSocketToken,
30
+ useValue: socket,
31
+ },
32
+ ]);
33
+ return {
34
+ gui: new RpgGui(context),
35
+ socket,
36
+ };
37
+ };
38
+
39
+ const CanvasGui = () => null;
40
+ const VueInventory = {
41
+ name: "inventory",
42
+ render() {
43
+ return null;
44
+ },
45
+ };
46
+ const VueDialog = {
47
+ name: PrebuiltGui.Dialog,
48
+ render() {
49
+ return null;
50
+ },
51
+ };
52
+ const VueMainMenu = {
53
+ name: PrebuiltGui.MainMenu,
54
+ render() {
55
+ return null;
56
+ },
57
+ };
58
+ const VueTooltip = {
59
+ name: "tooltip",
60
+ rpgAttachToSprite: true,
61
+ render() {
62
+ return null;
63
+ },
64
+ };
65
+
66
+ describe("RpgGui Vue integration", () => {
67
+ test("separates CanvasEngine and Vue GUI registries", async () => {
68
+ const { gui } = await createGui();
69
+
70
+ gui.add({
71
+ id: "canvas-tooltip",
72
+ component: CanvasGui,
73
+ attachToSprite: true,
74
+ });
75
+ gui.add({
76
+ id: "inventory",
77
+ component: VueInventory,
78
+ });
79
+ gui.add(VueTooltip);
80
+
81
+ expect(gui.get("canvas-tooltip")?.component).toBe(CanvasGui);
82
+ expect(gui.get("inventory")?.component).toBe(VueInventory);
83
+ expect(gui.get("tooltip")?.component).toBe(VueTooltip);
84
+ expect(gui.getAttachedGuis().map(item => item.name)).toEqual(["canvas-tooltip"]);
85
+ expect(gui.getAttachedVueGuis().map(item => item.name)).toEqual(["tooltip"]);
86
+ });
87
+
88
+ test("synchronizes Vue GUI display and hide states through the Vue bridge", async () => {
89
+ const { gui } = await createGui();
90
+ const bridge = {
91
+ updateGuiState: vi.fn(),
92
+ initializeGuiStates: vi.fn(),
93
+ };
94
+
95
+ gui.add({
96
+ id: "inventory",
97
+ component: VueInventory,
98
+ });
99
+ gui._setVueGuiInstance(bridge);
100
+
101
+ expect(bridge.initializeGuiStates).toHaveBeenCalledWith([
102
+ expect.objectContaining({
103
+ name: "inventory",
104
+ display: false,
105
+ data: {},
106
+ attachToSprite: false,
107
+ }),
108
+ ]);
109
+
110
+ gui.display("inventory", { gold: 12 });
111
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
112
+ expect.objectContaining({
113
+ name: "inventory",
114
+ display: true,
115
+ data: { gold: 12 },
116
+ attachToSprite: false,
117
+ }),
118
+ );
119
+
120
+ gui.hide("inventory");
121
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
122
+ expect.objectContaining({
123
+ name: "inventory",
124
+ display: false,
125
+ }),
126
+ );
127
+ });
128
+
129
+ test("waits for Vue GUI dependencies before display", async () => {
130
+ const { gui } = await createGui();
131
+ const bridge = {
132
+ updateGuiState: vi.fn(),
133
+ initializeGuiStates: vi.fn(),
134
+ };
135
+ const dependency = signal<any>(undefined);
136
+
137
+ gui.add({
138
+ id: "inventory",
139
+ component: VueInventory,
140
+ dependencies: () => [dependency],
141
+ });
142
+ gui._setVueGuiInstance(bridge);
143
+ gui.display("inventory", { items: ["potion"] });
144
+
145
+ expect(gui.isDisplaying("inventory")).toBe(false);
146
+ expect(bridge.updateGuiState).not.toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ display: true,
149
+ }),
150
+ );
151
+
152
+ dependency.set({ id: "player" });
153
+
154
+ expect(gui.isDisplaying("inventory")).toBe(true);
155
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
156
+ expect.objectContaining({
157
+ name: "inventory",
158
+ display: true,
159
+ data: { items: ["potion"] },
160
+ }),
161
+ );
162
+ });
163
+
164
+ test("allows Vue GUI entries to replace prebuilt CanvasEngine GUIs", async () => {
165
+ const { gui } = await createGui();
166
+ const bridge = {
167
+ updateGuiState: vi.fn(),
168
+ initializeGuiStates: vi.fn(),
169
+ };
170
+
171
+ gui._setVueGuiInstance(bridge);
172
+ gui.add({
173
+ id: PrebuiltGui.Dialog,
174
+ component: VueDialog,
175
+ });
176
+
177
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(VueDialog);
178
+ expect(gui.getAll()[PrebuiltGui.Dialog].component).toBe(VueDialog);
179
+ expect((gui as any).gui()[PrebuiltGui.Dialog]).toBeUndefined();
180
+ expect(gui.getVueGuis().filter(item => item.name === PrebuiltGui.Dialog)).toHaveLength(1);
181
+
182
+ gui.display(PrebuiltGui.Dialog, { text: "Hello" });
183
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
184
+ expect.objectContaining({
185
+ name: PrebuiltGui.Dialog,
186
+ display: true,
187
+ data: { text: "Hello" },
188
+ }),
189
+ );
190
+
191
+ gui.hide(PrebuiltGui.Dialog);
192
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
193
+ expect.objectContaining({
194
+ name: PrebuiltGui.Dialog,
195
+ display: false,
196
+ }),
197
+ );
198
+ });
199
+
200
+ test("allows CanvasEngine GUI entries to replace Vue GUI entries with the same id", async () => {
201
+ const { gui } = await createGui();
202
+ const bridge = {
203
+ updateGuiState: vi.fn(),
204
+ initializeGuiStates: vi.fn(),
205
+ };
206
+
207
+ gui._setVueGuiInstance(bridge);
208
+ gui.add({
209
+ id: PrebuiltGui.Dialog,
210
+ component: VueDialog,
211
+ });
212
+ gui.add({
213
+ id: PrebuiltGui.Dialog,
214
+ component: CanvasGui,
215
+ });
216
+
217
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(CanvasGui);
218
+ expect(gui.getVueGuis().some(item => item.name === PrebuiltGui.Dialog)).toBe(false);
219
+ expect((gui as any).gui()[PrebuiltGui.Dialog].component).toBe(CanvasGui);
220
+ expect(bridge.initializeGuiStates).toHaveBeenLastCalledWith([]);
221
+ });
222
+
223
+ test("keeps main menu optimistic reducers when a Vue GUI replaces the prebuilt component", async () => {
224
+ const { gui, socket } = await createGui();
225
+ const bridge = {
226
+ updateGuiState: vi.fn(),
227
+ initializeGuiStates: vi.fn(),
228
+ };
229
+
230
+ gui.add({
231
+ id: PrebuiltGui.MainMenu,
232
+ component: VueMainMenu,
233
+ });
234
+ gui._setVueGuiInstance(bridge);
235
+ gui.display(PrebuiltGui.MainMenu, {
236
+ items: [
237
+ {
238
+ id: "potion",
239
+ quantity: 2,
240
+ },
241
+ ],
242
+ });
243
+
244
+ gui.guiInteraction(PrebuiltGui.MainMenu, "useItem", { id: "potion" });
245
+
246
+ expect(gui.get(PrebuiltGui.MainMenu)?.data().items).toEqual([
247
+ {
248
+ id: "potion",
249
+ quantity: 1,
250
+ },
251
+ ]);
252
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
253
+ expect.objectContaining({
254
+ name: PrebuiltGui.MainMenu,
255
+ data: {
256
+ items: [
257
+ {
258
+ id: "potion",
259
+ quantity: 1,
260
+ },
261
+ ],
262
+ },
263
+ }),
264
+ );
265
+ expect(socket.emit).toHaveBeenCalledWith(
266
+ "gui.interaction",
267
+ expect.objectContaining({
268
+ guiId: PrebuiltGui.MainMenu,
269
+ name: "useItem",
270
+ }),
271
+ );
272
+ });
273
+ });