@rpgjs/client 5.0.0-beta.12 → 5.0.0-beta.13

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 (88) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/Game/Object.d.ts +2 -0
  3. package/dist/Game/Object.js +20 -6
  4. package/dist/Game/Object.js.map +1 -1
  5. package/dist/Gui/Gui.d.ts +3 -2
  6. package/dist/Gui/Gui.js +18 -6
  7. package/dist/Gui/Gui.js.map +1 -1
  8. package/dist/RpgClient.d.ts +21 -1
  9. package/dist/RpgClientEngine.d.ts +20 -2
  10. package/dist/RpgClientEngine.js +180 -17
  11. package/dist/RpgClientEngine.js.map +1 -1
  12. package/dist/components/character.ce.js +82 -7
  13. package/dist/components/character.ce.js.map +1 -1
  14. package/dist/components/gui/dialogbox/index.ce.js +27 -12
  15. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  16. package/dist/components/gui/gameover.ce.js +4 -3
  17. package/dist/components/gui/gameover.ce.js.map +1 -1
  18. package/dist/components/gui/menu/equip-menu.ce.js +9 -8
  19. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  20. package/dist/components/gui/menu/exit-menu.ce.js +7 -5
  21. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  22. package/dist/components/gui/menu/items-menu.ce.js +8 -7
  23. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  24. package/dist/components/gui/menu/main-menu.ce.js +12 -11
  25. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  26. package/dist/components/gui/menu/options-menu.ce.js +7 -5
  27. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  28. package/dist/components/gui/menu/skills-menu.ce.js +4 -2
  29. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  30. package/dist/components/gui/notification/notification.ce.js +4 -1
  31. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  32. package/dist/components/gui/save-load.ce.js +10 -9
  33. package/dist/components/gui/save-load.ce.js.map +1 -1
  34. package/dist/components/gui/shop/shop.ce.js +17 -16
  35. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  36. package/dist/components/gui/title-screen.ce.js +4 -3
  37. package/dist/components/gui/title-screen.ce.js.map +1 -1
  38. package/dist/components/interaction-components.ce.js +20 -0
  39. package/dist/components/interaction-components.ce.js.map +1 -0
  40. package/dist/components/scenes/canvas.ce.js +12 -7
  41. package/dist/components/scenes/canvas.ce.js.map +1 -1
  42. package/dist/components/scenes/draw-map.ce.js +18 -13
  43. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  44. package/dist/i18n.d.ts +55 -0
  45. package/dist/i18n.js +60 -0
  46. package/dist/i18n.js.map +1 -0
  47. package/dist/i18n.spec.d.ts +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +3 -1
  50. package/dist/module.js +23 -3
  51. package/dist/module.js.map +1 -1
  52. package/dist/services/interactions.d.ts +159 -0
  53. package/dist/services/interactions.js +460 -0
  54. package/dist/services/interactions.js.map +1 -0
  55. package/dist/services/interactions.spec.d.ts +1 -0
  56. package/dist/services/keyboardControls.d.ts +1 -0
  57. package/dist/services/keyboardControls.js +1 -0
  58. package/dist/services/keyboardControls.js.map +1 -1
  59. package/package.json +4 -4
  60. package/src/Game/Object.spec.ts +14 -1
  61. package/src/Game/Object.ts +34 -10
  62. package/src/Gui/Gui.spec.ts +67 -0
  63. package/src/Gui/Gui.ts +24 -7
  64. package/src/RpgClient.ts +28 -1
  65. package/src/RpgClientEngine.ts +248 -29
  66. package/src/components/character.ce +90 -7
  67. package/src/components/gui/dialogbox/index.ce +35 -14
  68. package/src/components/gui/gameover.ce +4 -3
  69. package/src/components/gui/menu/equip-menu.ce +9 -8
  70. package/src/components/gui/menu/exit-menu.ce +4 -3
  71. package/src/components/gui/menu/items-menu.ce +8 -7
  72. package/src/components/gui/menu/main-menu.ce +12 -11
  73. package/src/components/gui/menu/options-menu.ce +4 -3
  74. package/src/components/gui/menu/skills-menu.ce +2 -1
  75. package/src/components/gui/notification/notification.ce +7 -1
  76. package/src/components/gui/save-load.ce +11 -10
  77. package/src/components/gui/shop/shop.ce +17 -16
  78. package/src/components/gui/title-screen.ce +4 -3
  79. package/src/components/interaction-components.ce +23 -0
  80. package/src/components/scenes/canvas.ce +12 -7
  81. package/src/components/scenes/draw-map.ce +16 -5
  82. package/src/i18n.spec.ts +39 -0
  83. package/src/i18n.ts +59 -0
  84. package/src/index.ts +2 -0
  85. package/src/module.ts +32 -10
  86. package/src/services/interactions.spec.ts +175 -0
  87. package/src/services/interactions.ts +722 -0
  88. package/src/services/keyboardControls.ts +2 -1
@@ -64,6 +64,73 @@ const VueTooltip = {
64
64
  };
65
65
 
66
66
  describe("RpgGui Vue integration", () => {
67
+ test("tracks GUI open ids and sends them back when closing", async () => {
68
+ const { gui, socket } = await createGui();
69
+ await gui._initialize();
70
+ const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
71
+
72
+ openHandler({
73
+ guiId: PrebuiltGui.Dialog,
74
+ guiOpenId: "open-1",
75
+ data: { message: "Hello" },
76
+ });
77
+
78
+ expect(gui.get(PrebuiltGui.Dialog)?.openId).toBe("open-1");
79
+
80
+ gui.guiClose(PrebuiltGui.Dialog, 0, gui.get(PrebuiltGui.Dialog)?.openId);
81
+
82
+ expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
83
+ guiId: PrebuiltGui.Dialog,
84
+ guiOpenId: "open-1",
85
+ data: 0,
86
+ });
87
+ });
88
+
89
+ test("does not emit malformed GUI open ids", async () => {
90
+ const { gui, socket } = await createGui();
91
+
92
+ gui.guiClose(PrebuiltGui.Dialog, 0, (() => "open-1") as any);
93
+
94
+ expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
95
+ guiId: PrebuiltGui.Dialog,
96
+ guiOpenId: undefined,
97
+ data: 0,
98
+ });
99
+ });
100
+
101
+ test("ignores stale server close events for previous GUI opens", async () => {
102
+ const { gui, socket } = await createGui();
103
+ await gui._initialize();
104
+ const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
105
+ const exitHandler = socket.on.mock.calls.find(([event]) => event === "gui.exit")?.[1];
106
+
107
+ openHandler({
108
+ guiId: PrebuiltGui.Dialog,
109
+ guiOpenId: "open-1",
110
+ data: { message: "First" },
111
+ });
112
+ openHandler({
113
+ guiId: PrebuiltGui.Dialog,
114
+ guiOpenId: "open-2",
115
+ data: { message: "Second" },
116
+ });
117
+
118
+ exitHandler({
119
+ guiId: PrebuiltGui.Dialog,
120
+ guiOpenId: "open-1",
121
+ });
122
+
123
+ expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(true);
124
+ expect(gui.get(PrebuiltGui.Dialog)?.data()).toEqual({ message: "Second" });
125
+
126
+ exitHandler({
127
+ guiId: PrebuiltGui.Dialog,
128
+ guiOpenId: "open-2",
129
+ });
130
+
131
+ expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(false);
132
+ });
133
+
67
134
  test("separates CanvasEngine and Vue GUI registries", async () => {
68
135
  const { gui } = await createGui();
69
136
 
package/src/Gui/Gui.ts CHANGED
@@ -39,6 +39,7 @@ export interface GuiInstance {
39
39
  component: any;
40
40
  display: WritableSignal<boolean>;
41
41
  data: WritableSignal<any>;
42
+ openId?: string;
42
43
  autoDisplay: boolean;
43
44
  dependencies?: Signal[];
44
45
  subscription?: Subscription;
@@ -50,6 +51,7 @@ type GuiState = {
50
51
  component: any;
51
52
  display: boolean;
52
53
  data: any;
54
+ openId?: string;
53
55
  attachToSprite: boolean;
54
56
  };
55
57
 
@@ -179,12 +181,18 @@ export class RpgGui {
179
181
  }
180
182
 
181
183
  async _initialize() {
182
- this.webSocket.on("gui.open", (data: { guiId: string; data: any }) => {
184
+ this.webSocket.on("gui.open", (data: { guiId: string; data: any; guiOpenId?: string }) => {
183
185
  this.clearPendingActions(data.guiId);
184
- this.display(data.guiId, data.data);
186
+ this.display(data.guiId, data.data, [], data.guiOpenId);
185
187
  });
186
188
 
187
- this.webSocket.on("gui.exit", (guiId: string) => {
189
+ this.webSocket.on("gui.exit", (payload: string | { guiId: string; guiOpenId?: string }) => {
190
+ const guiId = typeof payload === "string" ? payload : payload.guiId;
191
+ const guiOpenId = typeof payload === "string" ? undefined : payload.guiOpenId;
192
+ const current = this.get(guiId);
193
+ if (guiOpenId && current?.openId && current.openId !== guiOpenId) {
194
+ return;
195
+ }
188
196
  this.hide(guiId);
189
197
  });
190
198
 
@@ -256,9 +264,12 @@ export class RpgGui {
256
264
  });
257
265
  }
258
266
 
259
- guiClose(guiId: string, data?: any) {
267
+ guiClose(guiId: string, data?: any, guiOpenId?: unknown) {
268
+ const normalizedOpenId =
269
+ typeof guiOpenId === "string" && guiOpenId.length > 0 ? guiOpenId : undefined;
260
270
  this.webSocket.emit("gui.exit", {
261
271
  guiId,
272
+ guiOpenId: normalizedOpenId,
262
273
  data,
263
274
  });
264
275
  }
@@ -308,6 +319,7 @@ export class RpgGui {
308
319
  component,
309
320
  display: signal<boolean>(gui.display || false),
310
321
  data: signal<any>(gui.data || {}),
322
+ openId: undefined,
311
323
  autoDisplay: gui.autoDisplay || false,
312
324
  dependencies: gui.dependencies ? gui.dependencies() : [],
313
325
  attachToSprite,
@@ -431,7 +443,7 @@ export class RpgGui {
431
443
  * gui.display('shop', { shopId: 1 }, [playerSignal, shopSignal]);
432
444
  * ```
433
445
  */
434
- display(id: string, data = {}, dependencies: Signal[] = []) {
446
+ display(id: string, data = {}, dependencies: Signal[] = [], openId?: string) {
435
447
  if (!this.exists(id)) {
436
448
  throw throwError(id);
437
449
  }
@@ -443,8 +455,9 @@ export class RpgGui {
443
455
 
444
456
  if (isVueComponent) {
445
457
  // Handle Vue component display
446
- this._handleVueComponentDisplay(id, data, dependencies, guiInstance);
458
+ this._handleVueComponentDisplay(id, data, dependencies, guiInstance, openId);
447
459
  } else {
460
+ guiInstance.openId = openId;
448
461
  guiInstance.data.set(data);
449
462
  guiInstance.display.set(true);
450
463
  }
@@ -464,7 +477,7 @@ export class RpgGui {
464
477
  * @param dependencies - Runtime dependencies
465
478
  * @param guiInstance - GUI instance
466
479
  */
467
- private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {
480
+ private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance, openId?: string) {
468
481
  // Unsubscribe from previous subscription if exists
469
482
  if (guiInstance.subscription) {
470
483
  guiInstance.subscription.unsubscribe();
@@ -482,6 +495,7 @@ export class RpgGui {
482
495
  deps.map(dependency => dependency.observable)
483
496
  ).subscribe((values) => {
484
497
  if (values.every(value => value !== undefined)) {
498
+ guiInstance.openId = openId;
485
499
  guiInstance.data.set(data);
486
500
  guiInstance.display.set(true);
487
501
  this._notifyVueGui(id, true, data);
@@ -491,6 +505,7 @@ export class RpgGui {
491
505
  }
492
506
 
493
507
  // No dependencies, display immediately
508
+ guiInstance.openId = openId;
494
509
  guiInstance.data.set(data);
495
510
  guiInstance.display.set(true);
496
511
  this._notifyVueGui(id, true, data);
@@ -523,6 +538,7 @@ export class RpgGui {
523
538
  }
524
539
 
525
540
  guiInstance.display.set(false)
541
+ guiInstance.openId = undefined;
526
542
 
527
543
  // Check if it's a Vue component and notify VueGui
528
544
  const isVueComponent = this.extraGuis.some(gui => gui.name === id);
@@ -573,6 +589,7 @@ export class RpgGui {
573
589
  component: gui.component,
574
590
  display,
575
591
  data,
592
+ openId: gui.openId,
576
593
  attachToSprite: gui.attachToSprite || false,
577
594
  };
578
595
  }
package/src/RpgClient.ts CHANGED
@@ -3,12 +3,16 @@ import { RpgClientEngine } from './RpgClientEngine'
3
3
  import { Loader, Container } from 'pixi.js'
4
4
  import { RpgClientObject } from './Game/Object'
5
5
  import type { RpgClientEvent } from './Game/Event'
6
- import { type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
6
+ import { type I18nMessages, type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
7
7
  import type {
8
8
  ClientProjectileSpawn,
9
9
  RenderedProjectileProps,
10
10
  } from './Game/ProjectileManager'
11
11
  import type { ClientVisualMap } from './Game/ClientVisuals'
12
+ import type {
13
+ RpgInteractionBehavior,
14
+ RpgInteractionMatcher,
15
+ } from './services/interactions'
12
16
 
13
17
  type RpgClass<T = any> = new (...args: any[]) => T
14
18
  type RpgComponent = RpgClientObject
@@ -363,6 +367,14 @@ export interface RpgProjectileHooks {
363
367
  }
364
368
 
365
369
  export interface RpgClient {
370
+ /**
371
+ * Default translations owned by this client module.
372
+ *
373
+ * Game-level translations provided with `provideI18n()` override module
374
+ * translations when they share the same locale and key.
375
+ */
376
+ i18n?: I18nMessages
377
+
366
378
  /**
367
379
  * Add hooks to the player or engine. All modules can listen to the hook
368
380
  *
@@ -796,4 +808,19 @@ export interface RpgClient {
796
808
  * compact spawn/impact/destroy events and the client predicts x/y locally.
797
809
  */
798
810
  projectiles?: RpgProjectileHooks
811
+
812
+ /**
813
+ * Client-only pointer interactions attached to sprites.
814
+ *
815
+ * Use this for hover popovers, selection, drag previews, cursor changes, and
816
+ * explicit mouse-driven gameplay actions. Pointer feedback stays local unless
817
+ * the behavior calls `ctx.action(...)`.
818
+ */
819
+ interactions?:
820
+ | ((engine: RpgClientEngine) => void)
821
+ | {
822
+ setup?: (engine: RpgClientEngine) => void
823
+ load?: (engine: RpgClientEngine) => void
824
+ use?: Array<[RpgInteractionMatcher, RpgInteractionBehavior | ComponentFunction]>
825
+ }
799
826
  }