@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
@@ -33,8 +33,8 @@
33
33
  height={engine.height}
34
34
  >
35
35
  @if (gui.display) {
36
- <gui.component data={gui.data} dependencies={gui.dependencies} onFinish={(data) => {
37
- onGuiFinish(gui, data)
36
+ <gui.component data={gui.data} dependencies={gui.dependencies} guiOpenId={gui.openId} onFinish={(data, guiOpenId) => {
37
+ onGuiFinish(gui, data, guiOpenId)
38
38
  }} onInteraction={(name, data) => {
39
39
  onGuiInteraction(gui, name, data)
40
40
  }} />
@@ -48,7 +48,6 @@
48
48
  import { inject } from "../../core/inject";
49
49
  import { RpgClientEngine } from "../../RpgClientEngine";
50
50
  import { RpgGui } from "../../Gui/Gui";
51
- import { delay } from "@rpgjs/common";
52
51
  import { NightAmbiant, SpriteShadows } from '@canvasengine/presets'
53
52
 
54
53
  const engine = inject(RpgClientEngine);
@@ -76,10 +75,16 @@
76
75
  }
77
76
  })
78
77
 
79
- const onGuiFinish = (gui, data) => {
80
- delay(() => {
81
- guiService.guiClose(gui.name, data)
82
- })
78
+ const normalizeOpenId = (value) => {
79
+ const resolved = typeof value === "function" ? value() : value
80
+ return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined
81
+ }
82
+
83
+ const onGuiFinish = (gui, data, guiOpenId) => {
84
+ const completedOpenId = normalizeOpenId(guiOpenId)
85
+ const currentOpenId = normalizeOpenId(gui.openId)
86
+ if (completedOpenId && currentOpenId && completedOpenId !== currentOpenId) return
87
+ guiService.guiClose(gui.name, data, completedOpenId ?? currentOpenId)
83
88
  }
84
89
 
85
90
  const onGuiInteraction = (gui, name, data) => {
@@ -1,5 +1,11 @@
1
- <Container sound={backgroundMusic} shake={shakeConfig} freeze={engine.gamePause}>
2
- <Container sound={backgroundAmbientSound} />
1
+ <Container shake={shakeConfig} freeze={engine.gamePause}>
2
+ @if (backgroundMusic()) {
3
+ <Container sound={backgroundMusic()} />
4
+ }
5
+
6
+ @if (backgroundAmbientSound()) {
7
+ <Container sound={backgroundAmbientSound()} />
8
+ }
3
9
 
4
10
  <Container>
5
11
  @if (map() && sceneComponent()) {
@@ -42,10 +48,15 @@
42
48
  const projectiles = engine.projectiles.current
43
49
  const map = engine.sceneMap?.data
44
50
  const sceneComponent = computed(() => map()?.component)
45
- const mapParams = map()?.params
46
51
  const weather = engine.sceneMap.weather
47
- const backgroundMusic = { src: mapParams?.backgroundMusic, autoplay: true, loop: true }
48
- const backgroundAmbientSound = { src: mapParams?.backgroundAmbientSound, autoplay: true, loop: true }
52
+ const backgroundMusic = computed(() => {
53
+ const src = map()?.params?.backgroundMusic
54
+ return src ? { src, autoplay: true, loop: true } : undefined
55
+ })
56
+ const backgroundAmbientSound = computed(() => {
57
+ const src = map()?.params?.backgroundAmbientSound
58
+ return src ? { src, autoplay: true, loop: true } : undefined
59
+ })
49
60
 
50
61
  const shakeConfig = {
51
62
  trigger: engine.mapShakeTrigger,
@@ -0,0 +1,39 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { Context, injector } from "@signe/di";
5
+ import { getOrCreateI18nService } from "@rpgjs/common";
6
+ import { provideClientModules } from "./module";
7
+ import { provideI18n } from "./i18n";
8
+
9
+ describe("client i18n", () => {
10
+ test("merges client module translations with game overrides", async () => {
11
+ const context = new Context();
12
+
13
+ await injector(context, [
14
+ provideClientModules([
15
+ {
16
+ i18n: {
17
+ fr: {
18
+ "menu.title": "Titre du module",
19
+ "menu.module-only": "Module",
20
+ },
21
+ },
22
+ },
23
+ ]),
24
+ provideI18n({
25
+ defaultLocale: "fr",
26
+ messages: {
27
+ fr: {
28
+ "menu.title": "Titre du jeu",
29
+ },
30
+ },
31
+ }),
32
+ ]);
33
+
34
+ const service = getOrCreateI18nService(context);
35
+
36
+ expect(service.t("menu.title", undefined, "fr")).toBe("Titre du jeu");
37
+ expect(service.t("menu.module-only", undefined, "fr")).toBe("Module");
38
+ });
39
+ });
package/src/i18n.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { createI18nProvider, type I18nConfig } from "@rpgjs/common";
2
+
3
+ export const RpgClientBuiltinI18n = {
4
+ en: {
5
+ "rpg.menu.title": "Menu",
6
+ "rpg.menu.status": "Status",
7
+ "rpg.menu.level": "Level",
8
+ "rpg.menu.gold": "Gold",
9
+ "rpg.menu.parameters": "Parameters",
10
+ "rpg.menu.items": "Items",
11
+ "rpg.menu.skills": "Skills",
12
+ "rpg.menu.equip": "Equip",
13
+ "rpg.menu.options": "Options",
14
+ "rpg.menu.save": "Save",
15
+ "rpg.menu.exit": "Exit",
16
+ "rpg.menu.weapons": "Weapons",
17
+ "rpg.menu.armor": "Armor",
18
+ "rpg.menu.use": "Use",
19
+ "rpg.menu.cancel": "Cancel",
20
+ "rpg.menu.unequip": "Unequip",
21
+ "rpg.menu.remove-equipment": "Remove the current equipment",
22
+ "rpg.menu.empty": "Empty",
23
+ "rpg.menu.leave-game": "Leave the game?",
24
+ "rpg.menu.exit-help": "Press Action to confirm or Escape to go back.",
25
+ "rpg.menu.options-help": "Configure your preferences here.",
26
+ "rpg.save.title": "Save Game",
27
+ "rpg.save.subtitle": "Choose a slot to overwrite or create.",
28
+ "rpg.load.title": "Load Game",
29
+ "rpg.load.subtitle": "Select a slot to load your progress.",
30
+ "rpg.save.auto": "Auto Save",
31
+ "rpg.save.slot": "Slot {index}",
32
+ "rpg.save.empty-slot": "Empty Slot",
33
+ "rpg.save.level": "Level",
34
+ "rpg.save.exp": "Exp",
35
+ "rpg.save.map": "Map",
36
+ "rpg.save.date": "Date",
37
+ "rpg.title.default": "RPG",
38
+ "rpg.title.start": "Start",
39
+ "rpg.title.load": "Load",
40
+ "rpg.gameover.title": "Game Over",
41
+ "rpg.gameover.title-screen": "Title Screen",
42
+ "rpg.gameover.load-game": "Load Game",
43
+ "rpg.shop.default-message": "Welcome to my shop!",
44
+ "rpg.shop.choose-action": "Choose an action",
45
+ "rpg.shop.buy": "Buy",
46
+ "rpg.shop.sell": "Sell",
47
+ "rpg.shop.back": "Back",
48
+ "rpg.shop.equipped": "Equipped",
49
+ "rpg.shop.already-equipped": "Already equipped",
50
+ "rpg.shop.qty": "Qty",
51
+ "rpg.shop.available": "Available",
52
+ "rpg.shop.quantity": "Quantity",
53
+ "rpg.shop.total": "Total",
54
+ },
55
+ };
56
+
57
+ export function provideI18n(config: I18nConfig = {}) {
58
+ return createI18nProvider(config);
59
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./core/inject";
8
8
  export * from "./services/loadMap";
9
9
  export * from "./services/actionInput";
10
10
  export * from "./services/pointerContext";
11
+ export * from "./services/interactions";
11
12
  export * from "./module";
12
13
  export * from "./Gui/Gui";
13
14
  export * from "./components/gui";
@@ -30,3 +31,4 @@ export * from "./Game/ProjectileManager";
30
31
  export * from "./Game/ClientVisuals";
31
32
  export { withMobile } from "./components/gui/mobile";
32
33
  export * from "./services/AbstractSocket";
34
+ export * from "./i18n";
package/src/module.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { findModules, provideModules } from "@rpgjs/common";
1
+ import { findModules, provideModules, registerI18nMessages } from "@rpgjs/common";
2
2
  import { FactoryProvider } from "@signe/di";
3
3
  import { RpgClientEngine } from "./RpgClientEngine";
4
4
  import { RpgClient } from "./RpgClient";
@@ -67,6 +67,9 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
67
67
  if ('client' in module) {
68
68
  module = module.client as any;
69
69
  }
70
+ if (module.i18n) {
71
+ registerI18nMessages(context, module.i18n, "client-module", 10);
72
+ }
70
73
  if (module.spritesheets) {
71
74
  const spritesheets = [...module.spritesheets];
72
75
  module.spritesheets = {
@@ -176,6 +179,25 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
176
179
  },
177
180
  };
178
181
  }
182
+ if (module.interactions) {
183
+ const interactions = module.interactions;
184
+ module.interactions = {
185
+ ...interactions,
186
+ load: (engine: RpgClientEngine) => {
187
+ if (typeof interactions === "function") {
188
+ interactions(engine);
189
+ return;
190
+ }
191
+ interactions.load?.(engine);
192
+ interactions.setup?.(engine);
193
+ if (Array.isArray(interactions.use)) {
194
+ interactions.use.forEach(([matcher, behavior]) => {
195
+ engine.interactions.use(matcher, behavior);
196
+ });
197
+ }
198
+ },
199
+ };
200
+ }
179
201
  if (module.transitions) {
180
202
  const transitions = [...module.transitions];
181
203
  module.transitions = {
@@ -243,15 +265,15 @@ export function provideGlobalConfig(config: any) {
243
265
  }
244
266
 
245
267
  export function provideClientGlobalConfig(config: any = {}) {
246
- if (!config.keyboardControls) {
247
- config.keyboardControls = {
248
- up: 'up',
249
- down: 'down',
250
- left: 'left',
251
- right: 'right',
252
- action: 'space',
253
- escape: 'escape'
254
- }
268
+ config.keyboardControls = {
269
+ up: 'up',
270
+ down: 'down',
271
+ left: 'left',
272
+ right: 'right',
273
+ action: 'space',
274
+ dash: 'shift',
275
+ escape: 'escape',
276
+ ...(config.keyboardControls ?? {}),
255
277
  }
256
278
  return provideGlobalConfig(config)
257
279
  }
@@ -0,0 +1,175 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import {
3
+ dragToTile,
4
+ hoverPopover,
5
+ RpgClientInteractions,
6
+ selectable,
7
+ } from "./interactions";
8
+ import { createClientPointerContext } from "./pointerContext";
9
+
10
+ function createClient() {
11
+ const client = {
12
+ pointer: createClientPointerContext(),
13
+ processAction: vi.fn(),
14
+ sceneMap: {
15
+ tileWidth: 16,
16
+ tileHeight: 16,
17
+ },
18
+ } as any;
19
+ client.interactions = new RpgClientInteractions(client);
20
+ return client;
21
+ }
22
+
23
+ describe("RpgClientInteractions", () => {
24
+ test("renders registered components with sprite state and bounds", () => {
25
+ const client = createClient();
26
+ const Popover = () => null;
27
+ const sprite = { id: "event-1", name: "Guard" };
28
+
29
+ client.interactions.use("Guard", hoverPopover(Popover, { label: "Talk" }));
30
+ client.interactions.handle(sprite, "pointerover", {
31
+ bounds: {
32
+ graphic: { left: 1, top: 2, right: 11, bottom: 22, width: 10, height: 20, centerX: 6, centerY: 12 } as any,
33
+ },
34
+ });
35
+
36
+ const entries = client.interactions.getRenderedComponents(sprite, {
37
+ graphic: { left: 1, top: 2, right: 11, bottom: 22, width: 10, height: 20, centerX: 6, centerY: 12 } as any,
38
+ });
39
+
40
+ expect(entries).toHaveLength(1);
41
+ expect(entries[0].component).toBe(Popover);
42
+ expect(entries[0].props.label).toBe("Talk");
43
+ expect(entries[0].props.state.hovered).toBe(true);
44
+ expect(entries[0].props.bounds.centerX).toBe(6);
45
+ });
46
+
47
+ test("keeps clicks client-only unless a behavior sends an action", () => {
48
+ const client = createClient();
49
+ const sprite = { id: "event-1", name: "Guard" };
50
+
51
+ client.interactions.use("Guard", selectable());
52
+ client.interactions.handle(sprite, "click");
53
+
54
+ expect(client.interactions.getState(sprite).selected).toBe(true);
55
+ expect(client.processAction).not.toHaveBeenCalled();
56
+
57
+ client.interactions.use("Guard", {
58
+ click(ctx) {
59
+ ctx.action("guard:talk", { eventId: ctx.target.id });
60
+ },
61
+ });
62
+ client.interactions.handle(sprite, "click");
63
+
64
+ expect(client.processAction).toHaveBeenCalledWith("guard:talk", { eventId: "event-1" });
65
+ });
66
+
67
+ test("uses behavior hit tests before changing hover state", () => {
68
+ const client = createClient();
69
+ const sprite = { id: "event-1", name: "Tree" };
70
+
71
+ client.pointer.update({ x: 0, y: 0 }, { x: 40, y: 40 });
72
+ client.interactions.use("Tree", {
73
+ cursor: "pointer",
74
+ hitTest(ctx) {
75
+ return ctx.bounds("hitbox").contains(ctx.pointer.world());
76
+ },
77
+ });
78
+
79
+ client.interactions.handle(sprite, "pointerover", {
80
+ bounds: {
81
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
82
+ },
83
+ });
84
+
85
+ expect(client.interactions.getState(sprite).hovered).toBe(false);
86
+ expect(client.interactions.cursorFor(sprite, {
87
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
88
+ })).toBeUndefined();
89
+ });
90
+
91
+ test("exposes handler bounds in world coordinates", () => {
92
+ const client = createClient();
93
+ const sprite = { id: "crate-1", name: "Crate", x: () => 100, y: () => 80 };
94
+
95
+ client.pointer.update({ x: 0, y: 0 }, { x: 112, y: 92 });
96
+ client.interactions.use("Crate", {
97
+ component: () => null,
98
+ cursor: "grab",
99
+ hitTest(ctx) {
100
+ return ctx.bounds("hitbox").contains(ctx.pointer.world());
101
+ },
102
+ });
103
+
104
+ client.interactions.handle(sprite, "pointerover", {
105
+ bounds: {
106
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
107
+ },
108
+ });
109
+
110
+ expect(client.interactions.getState(sprite).hovered).toBe(true);
111
+ expect(client.interactions.cursorFor(sprite, {
112
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
113
+ })).toBe("grab");
114
+
115
+ const [entry] = client.interactions.getRenderedComponents(sprite, {
116
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
117
+ });
118
+ expect(entry?.props.bounds.centerX).toBe(8);
119
+ });
120
+
121
+ test("updates hit-tested hover while moving inside an already hovered sprite", () => {
122
+ const client = createClient();
123
+ const sprite = { id: "crate-1", name: "Crate", x: () => 100, y: () => 80 };
124
+
125
+ client.interactions.use("Crate", {
126
+ hitTest(ctx) {
127
+ return ctx.bounds("hitbox").contains(ctx.pointer.world());
128
+ },
129
+ });
130
+
131
+ client.pointer.update({ x: 0, y: 0 }, { x: 140, y: 120 });
132
+ client.interactions.handle(sprite, "pointerover", {
133
+ bounds: {
134
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
135
+ },
136
+ });
137
+ expect(client.interactions.getState(sprite).hovered).toBe(false);
138
+
139
+ client.pointer.update({ x: 0, y: 0 }, { x: 112, y: 92 });
140
+ client.interactions.handle(sprite, "pointermove", {
141
+ bounds: {
142
+ hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
143
+ },
144
+ });
145
+
146
+ expect(client.interactions.getState(sprite).hovered).toBe(true);
147
+ });
148
+
149
+ test("runs drag lifecycle and resolves pointer tile on drop", () => {
150
+ const client = createClient();
151
+ const sprite = { id: "crate-1", name: "Crate" };
152
+
153
+ client.interactions.use("Crate", dragToTile({ action: "crate:move" }));
154
+ client.pointer.update({ x: 0, y: 0 }, { x: 18, y: 35 });
155
+ client.interactions.handle(sprite, "pointerdown");
156
+
157
+ expect(client.interactions.getState(sprite).dragging).toBe(true);
158
+
159
+ client.pointer.update({ x: 0, y: 0 }, { x: 33, y: 47 });
160
+ client.interactions.handlePointerUp();
161
+
162
+ expect(client.interactions.getState(sprite).dragging).toBe(false);
163
+ expect(client.processAction).toHaveBeenCalledWith("crate:move", {
164
+ eventId: "crate-1",
165
+ position: {
166
+ x: 2,
167
+ y: 2,
168
+ worldX: 32,
169
+ worldY: 32,
170
+ width: 16,
171
+ height: 16,
172
+ },
173
+ });
174
+ });
175
+ });