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

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 (154) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/Game/AnimationManager.d.ts +1 -0
  3. package/dist/Game/AnimationManager.js +3 -0
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/ClientVisuals.d.ts +61 -0
  6. package/dist/Game/ClientVisuals.js +96 -0
  7. package/dist/Game/ClientVisuals.js.map +1 -0
  8. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  9. package/dist/Game/EventComponentResolver.d.ts +16 -0
  10. package/dist/Game/EventComponentResolver.js +52 -0
  11. package/dist/Game/EventComponentResolver.js.map +1 -0
  12. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  13. package/dist/Game/Map.js +9 -0
  14. package/dist/Game/Map.js.map +1 -1
  15. package/dist/Game/Object.js +2 -2
  16. package/dist/Game/Object.js.map +1 -1
  17. package/dist/Game/Object.spec.d.ts +1 -0
  18. package/dist/Game/ProjectileManager.d.ts +98 -0
  19. package/dist/Game/ProjectileManager.js +196 -0
  20. package/dist/Game/ProjectileManager.js.map +1 -0
  21. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  22. package/dist/RpgClient.d.ts +117 -13
  23. package/dist/RpgClientEngine.d.ts +82 -4
  24. package/dist/RpgClientEngine.js +296 -51
  25. package/dist/RpgClientEngine.js.map +1 -1
  26. package/dist/components/animations/fx.ce.js +58 -0
  27. package/dist/components/animations/fx.ce.js.map +1 -0
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/animations/index.d.ts +1 -0
  30. package/dist/components/animations/index.js +3 -1
  31. package/dist/components/animations/index.js.map +1 -1
  32. package/dist/components/character.ce.js +140 -40
  33. package/dist/components/character.ce.js.map +1 -1
  34. package/dist/components/dynamics/bar.ce.js +4 -3
  35. package/dist/components/dynamics/bar.ce.js.map +1 -1
  36. package/dist/components/dynamics/image.ce.js +2 -1
  37. package/dist/components/dynamics/image.ce.js.map +1 -1
  38. package/dist/components/dynamics/shape.ce.js +3 -2
  39. package/dist/components/dynamics/shape.ce.js.map +1 -1
  40. package/dist/components/dynamics/text.ce.js +9 -8
  41. package/dist/components/dynamics/text.ce.js.map +1 -1
  42. package/dist/components/gui/dialogbox/index.ce.js +3 -2
  43. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  44. package/dist/components/gui/gameover.ce.js +3 -2
  45. package/dist/components/gui/gameover.ce.js.map +1 -1
  46. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  47. package/dist/components/gui/menu/equip-menu.ce.js +2 -1
  48. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  49. package/dist/components/gui/menu/exit-menu.ce.js +2 -1
  50. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  51. package/dist/components/gui/menu/items-menu.ce.js +3 -2
  52. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/main-menu.ce.js +3 -2
  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/mobile.ce.js.map +1 -1
  58. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  59. package/dist/components/gui/save-load.ce.js +2 -1
  60. package/dist/components/gui/save-load.ce.js.map +1 -1
  61. package/dist/components/gui/shop/shop.ce.js +3 -2
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js +3 -2
  64. package/dist/components/gui/title-screen.ce.js.map +1 -1
  65. package/dist/components/index.d.ts +2 -1
  66. package/dist/components/index.js +1 -0
  67. package/dist/components/player-components.ce.js +11 -10
  68. package/dist/components/player-components.ce.js.map +1 -1
  69. package/dist/components/prebuilt/hp-bar.ce.js +4 -3
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js +2 -1
  72. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  73. package/dist/components/scenes/canvas.ce.js +12 -4
  74. package/dist/components/scenes/canvas.ce.js.map +1 -1
  75. package/dist/components/scenes/draw-map.ce.js +6 -3
  76. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  77. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  78. package/dist/index.d.ts +4 -0
  79. package/dist/index.js +10 -5
  80. package/dist/module.js +18 -0
  81. package/dist/module.js.map +1 -1
  82. package/dist/services/actionInput.d.ts +14 -0
  83. package/dist/services/actionInput.js +59 -0
  84. package/dist/services/actionInput.js.map +1 -0
  85. package/dist/services/actionInput.spec.d.ts +1 -0
  86. package/dist/services/mmorpg-connection.d.ts +5 -0
  87. package/dist/services/mmorpg-connection.js +50 -0
  88. package/dist/services/mmorpg-connection.js.map +1 -0
  89. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  90. package/dist/services/mmorpg.d.ts +10 -4
  91. package/dist/services/mmorpg.js +48 -30
  92. package/dist/services/mmorpg.js.map +1 -1
  93. package/dist/services/pointerContext.d.ts +11 -0
  94. package/dist/services/pointerContext.js +48 -0
  95. package/dist/services/pointerContext.js.map +1 -0
  96. package/dist/services/pointerContext.spec.d.ts +1 -0
  97. package/dist/services/standalone-message.d.ts +1 -0
  98. package/dist/services/standalone-message.js +9 -0
  99. package/dist/services/standalone-message.js.map +1 -0
  100. package/dist/services/standalone.d.ts +3 -1
  101. package/dist/services/standalone.js +34 -15
  102. package/dist/services/standalone.js.map +1 -1
  103. package/dist/services/standalone.spec.d.ts +1 -0
  104. package/dist/utils/mapId.d.ts +1 -0
  105. package/dist/utils/mapId.js +6 -0
  106. package/dist/utils/mapId.js.map +1 -0
  107. package/package.json +7 -7
  108. package/src/Game/AnimationManager.ts +4 -0
  109. package/src/Game/ClientVisuals.spec.ts +56 -0
  110. package/src/Game/ClientVisuals.ts +184 -0
  111. package/src/Game/EventComponentResolver.spec.ts +84 -0
  112. package/src/Game/EventComponentResolver.ts +74 -0
  113. package/src/Game/Map.ts +10 -0
  114. package/src/Game/Object.spec.ts +46 -0
  115. package/src/Game/Object.ts +2 -2
  116. package/src/Game/ProjectileManager.spec.ts +449 -0
  117. package/src/Game/ProjectileManager.ts +346 -0
  118. package/src/RpgClient.ts +130 -15
  119. package/src/RpgClientEngine.ts +405 -69
  120. package/src/components/animations/fx.ce +101 -0
  121. package/src/components/animations/index.ts +4 -2
  122. package/src/components/character.ce +185 -40
  123. package/src/components/dynamics/bar.ce +4 -3
  124. package/src/components/dynamics/image.ce +2 -1
  125. package/src/components/dynamics/shape.ce +3 -2
  126. package/src/components/dynamics/text.ce +9 -8
  127. package/src/components/gui/dialogbox/index.ce +3 -2
  128. package/src/components/gui/gameover.ce +2 -1
  129. package/src/components/gui/menu/equip-menu.ce +2 -1
  130. package/src/components/gui/menu/exit-menu.ce +2 -1
  131. package/src/components/gui/menu/items-menu.ce +3 -2
  132. package/src/components/gui/menu/main-menu.ce +2 -1
  133. package/src/components/gui/save-load.ce +2 -1
  134. package/src/components/gui/shop/shop.ce +3 -2
  135. package/src/components/gui/title-screen.ce +2 -1
  136. package/src/components/index.ts +2 -1
  137. package/src/components/player-components.ce +11 -10
  138. package/src/components/prebuilt/hp-bar.ce +4 -3
  139. package/src/components/prebuilt/light-halo.ce +2 -2
  140. package/src/components/scenes/canvas.ce +10 -2
  141. package/src/components/scenes/draw-map.ce +17 -3
  142. package/src/index.ts +4 -0
  143. package/src/module.ts +24 -0
  144. package/src/services/actionInput.spec.ts +155 -0
  145. package/src/services/actionInput.ts +120 -0
  146. package/src/services/mmorpg-connection.spec.ts +99 -0
  147. package/src/services/mmorpg-connection.ts +69 -0
  148. package/src/services/mmorpg.ts +60 -34
  149. package/src/services/pointerContext.spec.ts +36 -0
  150. package/src/services/pointerContext.ts +84 -0
  151. package/src/services/standalone-message.ts +7 -0
  152. package/src/services/standalone.spec.ts +34 -0
  153. package/src/services/standalone.ts +42 -12
  154. package/src/utils/mapId.ts +2 -0
@@ -90,6 +90,7 @@
90
90
  import { signal, computed, createTabindexNavigator, effect } from "canvasengine";
91
91
  import { inject } from "../../../core/inject";
92
92
  import { RpgClientEngine } from "../../../RpgClientEngine";
93
+ import { getKeyboardControlBind } from "../../../services/actionInput";
93
94
 
94
95
  const engine = inject(RpgClientEngine);
95
96
  const keyboardControls = engine.globalConfig.keyboardControls;
@@ -380,7 +381,7 @@
380
381
  }
381
382
  },
382
383
  action: {
383
- bind: keyboardControls.action,
384
+ bind: getKeyboardControlBind(keyboardControls.action),
384
385
  keyDown() {
385
386
  if (!listEntries().length) return;
386
387
  commitSelection(selectedItem());
@@ -16,6 +16,7 @@
16
16
  import { signal } from "canvasengine";
17
17
  import { inject } from "../../../core/inject";
18
18
  import { RpgClientEngine } from "../../../RpgClientEngine";
19
+ import { getKeyboardControlBind } from "../../../services/actionInput";
19
20
 
20
21
  const engine = inject(RpgClientEngine);
21
22
  const keyboardControls = engine.globalConfig.keyboardControls;
@@ -23,7 +24,7 @@
23
24
 
24
25
  const controls = signal({
25
26
  action: {
26
- bind: keyboardControls.action,
27
+ bind: getKeyboardControlBind(keyboardControls.action),
27
28
  keyDown() {
28
29
  if (onConfirm) onConfirm();
29
30
  }
@@ -90,6 +90,7 @@
90
90
  import { inject } from "../../../core/inject";
91
91
  import { RpgClientEngine } from "../../../RpgClientEngine";
92
92
  import { delay } from "@rpgjs/common";
93
+ import { getKeyboardControlBind } from "../../../services/actionInput";
93
94
 
94
95
  const engine = inject(RpgClientEngine);
95
96
  const keyboardControls = engine.globalConfig.keyboardControls;
@@ -226,7 +227,7 @@
226
227
  }
227
228
  },
228
229
  action: {
229
- bind: keyboardControls.action,
230
+ bind: getKeyboardControlBind(keyboardControls.action),
230
231
  keyDown() {
231
232
  if (!confirmOpen()) return;
232
233
  confirmSelect(selectedConfirm())();
@@ -289,7 +290,7 @@
289
290
  }
290
291
  },
291
292
  action: {
292
- bind: keyboardControls.action,
293
+ bind: getKeyboardControlBind(keyboardControls.action),
293
294
  keyDown() {
294
295
  if (confirmOpen()) {
295
296
  confirmSelect(selectedConfirm())();
@@ -101,6 +101,7 @@
101
101
  import ExitMenu from "./exit-menu.ce";
102
102
  import { getEntityProp } from "../../../utils/getEntityProp";
103
103
  import { delay } from "@rpgjs/common";
104
+ import { getKeyboardControlBind } from "../../../services/actionInput";
104
105
 
105
106
  const engine = inject(RpgClientEngine);
106
107
  const currentPlayer = engine.scene.currentPlayer;
@@ -259,7 +260,7 @@
259
260
  }
260
261
  },
261
262
  action: {
262
- bind: keyboardControls.action,
263
+ bind: getKeyboardControlBind(keyboardControls.action),
263
264
  keyDown() {
264
265
  if (saveOverlay()) return;
265
266
  if (view() !== "menu") return;
@@ -40,6 +40,7 @@
40
40
  import { SaveClientService } from "../../services/save";
41
41
  import { PrebuiltGui } from "@rpgjs/common";
42
42
  import { RpgGui } from "../../Gui/Gui";
43
+ import { getKeyboardControlBind } from "../../services/actionInput";
43
44
 
44
45
  const engine = inject(RpgClientEngine);
45
46
  const saveClient = inject(SaveClientService);
@@ -185,7 +186,7 @@
185
186
  }
186
187
  },
187
188
  action: {
188
- bind: keyboardControls.action,
189
+ bind: getKeyboardControlBind(keyboardControls.action),
189
190
  keyDown() {
190
191
  triggerSelect(selectedSlot());
191
192
  }
@@ -180,6 +180,7 @@
180
180
  import { mount, signal, computed, createTabindexNavigator, effect } from "canvasengine";
181
181
  import { inject } from "../../../core/inject";
182
182
  import { RpgClientEngine } from "../../../RpgClientEngine";
183
+ import { getKeyboardControlBind } from "../../../services/actionInput";
183
184
 
184
185
  const engine = inject(RpgClientEngine)
185
186
  const currentPlayer = engine.scene.currentPlayer
@@ -387,7 +388,7 @@
387
388
  }
388
389
  },
389
390
  action: {
390
- bind: keyboardControls.action,
391
+ bind: getKeyboardControlBind(keyboardControls.action),
391
392
  keyDown() {
392
393
  const mode = selectedModeIndex() === 0 ? 'buy' : 'sell'
393
394
  tradeMode.set(mode)
@@ -433,7 +434,7 @@
433
434
  }
434
435
  },
435
436
  action: {
436
- bind: keyboardControls.action,
437
+ bind: getKeyboardControlBind(keyboardControls.action),
437
438
  keyDown() {
438
439
  if (quantityDialogOpen()) {
439
440
  const item = currentItem()
@@ -31,6 +31,7 @@
31
31
  import { inject } from "../../core/inject";
32
32
  import { RpgClientEngine } from "../../RpgClientEngine";
33
33
  import { RpgGui } from "../../Gui/Gui";
34
+ import { getKeyboardControlBind } from "../../services/actionInput";
34
35
 
35
36
  const engine = inject(RpgClientEngine);
36
37
  const guiService = inject(RpgGui);
@@ -150,7 +151,7 @@
150
151
  }
151
152
  },
152
153
  action: {
153
- bind: keyboardControls.action,
154
+ bind: getKeyboardControlBind(keyboardControls.action),
154
155
  keyDown() {
155
156
  if (guiService.isDisplaying(PrebuiltGui.Save)) return;
156
157
  triggerSelect(selectedEntry());
@@ -1,7 +1,8 @@
1
1
  import EventLayerComponent from "./scenes/event-layer.ce";
2
+ import SceneMap from "./scenes/draw-map.ce";
2
3
  import CharacterComponent from "./character.ce";
3
4
 
4
5
  // Prebuilt sprite components
5
6
  export { HpBar } from "./prebuilt";
6
7
 
7
- export { EventLayerComponent, CharacterComponent }
8
+ export { EventLayerComponent, SceneMap, CharacterComponent }
@@ -29,7 +29,7 @@
29
29
  justifyContent="center"
30
30
  alignItems="center"
31
31
  >
32
- <entry.component object ...entry.props />
32
+ <entry.component object={sprite} ...entry.props />
33
33
  </Container>
34
34
  }
35
35
  </Container>
@@ -54,6 +54,7 @@ const { object, position, graphicBounds } = defineProps({
54
54
  default: 'top'
55
55
  }
56
56
  });
57
+ const sprite = object();
57
58
 
58
59
  const client = inject(RpgClientEngine);
59
60
  const warnedComponents = new Set();
@@ -63,16 +64,16 @@ const readPosition = computed(() => position?.() ?? 'top');
63
64
  const componentSource = computed(() => {
64
65
  switch (readPosition()) {
65
66
  case 'bottom':
66
- return object.componentsBottom?.();
67
+ return sprite.componentsBottom?.();
67
68
  case 'center':
68
- return object.componentsCenter?.();
69
+ return sprite.componentsCenter?.();
69
70
  case 'left':
70
- return object.componentsLeft?.();
71
+ return sprite.componentsLeft?.();
71
72
  case 'right':
72
- return object.componentsRight?.();
73
+ return sprite.componentsRight?.();
73
74
  case 'top':
74
75
  default:
75
- return object.componentsTop?.();
76
+ return sprite.componentsTop?.();
76
77
  }
77
78
  });
78
79
 
@@ -98,16 +99,16 @@ const componentData = computed(() => {
98
99
 
99
100
  const layout = computed(() => componentData()?.layout ?? {});
100
101
  const rows = computed(() => componentData()?.components ?? []);
101
- const hitbox = object.hitbox;
102
+ const hitbox = sprite.hitbox;
102
103
 
103
104
  const toNumber = (value, fallback = 0) => {
104
- const resolved = resolveDynamicValue(value, object);
105
+ const resolved = resolveDynamicValue(value, sprite);
105
106
  const num = typeof resolved === 'number' ? resolved : parseFloat(resolved);
106
107
  return Number.isFinite(num) ? num : fallback;
107
108
  };
108
109
 
109
110
  const estimateTextWidth = (value, style = {}) => {
110
- const text = String(resolveDynamicValue(value ?? '', object) ?? '');
111
+ const text = String(resolveDynamicValue(value ?? '', sprite) ?? '');
111
112
  const fontSize = toNumber(style.fontSize, 12);
112
113
  return Math.max(1, Math.ceil(text.length * fontSize * 0.6));
113
114
  };
@@ -206,7 +207,7 @@ const renderedRows = computed(() => {
206
207
 
207
208
  entries.push({
208
209
  component,
209
- props: resolveDynamicProps(getComponentProps(definition), object),
210
+ props: resolveDynamicProps(getComponentProps(definition), sprite),
210
211
  width: cell.width,
211
212
  height: cell.height
212
213
  });
@@ -46,6 +46,7 @@
46
46
  import { computed, animatedSignal, effect } from "canvasengine";
47
47
 
48
48
  const { object } = defineProps();
49
+ const sprite = object();
49
50
 
50
51
  // ====================
51
52
  // Configuration
@@ -93,14 +94,14 @@ const highlightHeight = Math.floor(fillHeight / 2);
93
94
  // ====================
94
95
 
95
96
  /** Gets hitbox dimensions for positioning */
96
- const hitbox = object.hitbox;
97
+ const hitbox = sprite.hitbox;
97
98
 
98
99
  /**
99
100
  * Gets the current HP value from the player object
100
101
  * Uses hpSignal which is synchronized from the server
101
102
  */
102
103
  const currentHp = computed(() => {
103
- return object.hpSignal?.() ?? 0;
104
+ return sprite.hpSignal?.() ?? 0;
104
105
  });
105
106
 
106
107
  /**
@@ -108,7 +109,7 @@ const currentHp = computed(() => {
108
109
  * Reads from _param.maxHp which contains calculated stats
109
110
  */
110
111
  const maxHp = computed(() => {
111
- const params = object._param?.() ?? {};
112
+ const params = sprite._param?.() ?? {};
112
113
  return params.maxHp ?? 100;
113
114
  });
114
115
 
@@ -31,6 +31,7 @@ const {
31
31
  opacitySpeed,
32
32
  lightColor
33
33
  } = defineProps();
34
+ const sprite = object();
34
35
 
35
36
  // ====================
36
37
  // Props with default values
@@ -92,7 +93,7 @@ const currentOpacity = computed(() => {
92
93
  // Position calculations
93
94
  // ====================
94
95
 
95
- const hitbox = object.hitbox;
96
+ const hitbox = sprite.hitbox;
96
97
 
97
98
  const position = computed(() => ({
98
99
  x: hitbox().w / 2,
@@ -145,4 +146,3 @@ tick(() => {
145
146
  time.update(t => t + 1);
146
147
  });
147
148
  </script>
148
-
@@ -23,7 +23,15 @@
23
23
  <SceneMap />
24
24
  </Viewport>
25
25
  @for (gui of guiList) {
26
- <Container display="flex">
26
+ <Container
27
+ positionType="absolute"
28
+ top={0}
29
+ left={0}
30
+ right={0}
31
+ bottom={0}
32
+ width={engine.width}
33
+ height={engine.height}
34
+ >
27
35
  @if (gui.display) {
28
36
  <gui.component data={gui.data} dependencies={gui.dependencies} onFinish={(data) => {
29
37
  onGuiFinish(gui, data)
@@ -39,12 +47,12 @@
39
47
  import { computed, effect } from "canvasengine";
40
48
  import { inject } from "../../core/inject";
41
49
  import { RpgClientEngine } from "../../RpgClientEngine";
42
- import SceneMap from './draw-map.ce'
43
50
  import { RpgGui } from "../../Gui/Gui";
44
51
  import { delay } from "@rpgjs/common";
45
52
  import { NightAmbiant, SpriteShadows } from '@canvasengine/presets'
46
53
 
47
54
  const engine = inject(RpgClientEngine);
55
+ const SceneMap = engine.sceneMapComponent;
48
56
  const guiService = inject(RpgGui);
49
57
  const sceneData = engine.sceneMap.data
50
58
  const lighting = engine.sceneMap.lighting
@@ -1,8 +1,14 @@
1
1
  <Container sound={backgroundMusic} shake={shakeConfig} freeze={engine.gamePause}>
2
2
  <Container sound={backgroundAmbientSound} />
3
3
 
4
- @if (map() && sceneComponent()) {
5
- <sceneComponent() data={map().data} params={map().params} />
4
+ <Container>
5
+ @if (map() && sceneComponent()) {
6
+ <sceneComponent() data={map().data} params={map().params} />
7
+ }
8
+ </Container>
9
+
10
+ @for (child of children) {
11
+ <child />
6
12
  }
7
13
 
8
14
  @for (componentAnimation of componentAnimations) {
@@ -13,19 +19,27 @@
13
19
  </Container>
14
20
  }
15
21
 
22
+ <Container sortableChildren={true}>
23
+ @for (projectile of projectiles() ; track projectile.props.id) {
24
+ <projectile.component ...projectile.props />
25
+ }
26
+ </Container>
27
+
16
28
  @if (weatherProps()) {
17
29
  <Weather ...weatherProps() />
18
30
  }
19
31
  </Container>
20
32
 
21
33
  <script>
22
- import { computed } from 'canvasengine'
34
+ import { computed, effect } from 'canvasengine'
23
35
  import { inject } from "../../core/inject";
24
36
  import { RpgClientEngine } from "../../RpgClientEngine";
25
37
  import { Weather } from '@canvasengine/presets'
26
38
 
39
+ const { children } = defineProps()
27
40
  const engine = inject(RpgClientEngine);
28
41
  const componentAnimations = engine.componentAnimations
42
+ const projectiles = engine.projectiles.current
29
43
  const map = engine.sceneMap?.data
30
44
  const sceneComponent = computed(() => map()?.component)
31
45
  const mapParams = map()?.params
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export * from "./services/save";
6
6
  export * from "./core/setup";
7
7
  export * from "./core/inject";
8
8
  export * from "./services/loadMap";
9
+ export * from "./services/actionInput";
10
+ export * from "./services/pointerContext";
9
11
  export * from "./module";
10
12
  export * from "./Gui/Gui";
11
13
  export * from "./components/gui";
@@ -24,5 +26,7 @@ export { Control } from "./services/keyboardControls";
24
26
  export { RpgClientObject } from "./Game/Object";
25
27
  export { RpgClientPlayer } from "./Game/Player";
26
28
  export { RpgClientEvent } from "./Game/Event";
29
+ export * from "./Game/ProjectileManager";
30
+ export * from "./Game/ClientVisuals";
27
31
  export { withMobile } from "./components/gui/mobile";
28
32
  export * from "./services/AbstractSocket";
package/src/module.ts CHANGED
@@ -155,6 +155,27 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
155
155
  },
156
156
  };
157
157
  }
158
+ if (module.clientVisuals) {
159
+ const clientVisuals = { ...module.clientVisuals };
160
+ module.clientVisuals = {
161
+ load: (engine: RpgClientEngine) => {
162
+ engine.registerClientVisuals(clientVisuals);
163
+ },
164
+ };
165
+ }
166
+ if (module.projectiles) {
167
+ const projectiles = { ...module.projectiles };
168
+ module.projectiles = {
169
+ ...projectiles,
170
+ load: (engine: RpgClientEngine) => {
171
+ if (projectiles.components) {
172
+ Object.entries(projectiles.components).forEach(([type, component]) => {
173
+ engine.registerProjectileComponent(type, component);
174
+ });
175
+ }
176
+ },
177
+ };
178
+ }
158
179
  if (module.transitions) {
159
180
  const transitions = [...module.transitions];
160
181
  module.transitions = {
@@ -200,6 +221,9 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
200
221
  engine.registerSpriteComponent(id, component);
201
222
  });
202
223
  }
224
+ if (sprite.eventComponent) {
225
+ engine.addEventComponentResolver(sprite.eventComponent);
226
+ }
203
227
  },
204
228
  };
205
229
  }
@@ -0,0 +1,155 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ getKeyboardControlBind,
4
+ keyboardEventMatchesBind,
5
+ normalizeActionInput,
6
+ resolveKeyboardActionInput,
7
+ resolveKeyboardDirectionInput,
8
+ } from "./actionInput";
9
+
10
+ const keyboardEvent = (values: Partial<KeyboardEvent>) =>
11
+ values as KeyboardEvent;
12
+
13
+ describe("normalizeActionInput", () => {
14
+ test("keeps simple actions compatible", () => {
15
+ expect(normalizeActionInput("action")).toEqual({
16
+ action: "action",
17
+ });
18
+ });
19
+
20
+ test("adds custom data to action payloads", () => {
21
+ expect(normalizeActionInput("projectile:shoot", {
22
+ target: { x: 320, y: 180 },
23
+ source: "map-click",
24
+ })).toEqual({
25
+ action: "projectile:shoot",
26
+ data: {
27
+ target: { x: 320, y: 180 },
28
+ source: "map-click",
29
+ },
30
+ });
31
+ });
32
+
33
+ test("keeps object-form action payloads intact", () => {
34
+ const payload = {
35
+ action: "projectile:shoot",
36
+ data: { target: { x: 64, y: 96 } },
37
+ };
38
+
39
+ expect(normalizeActionInput(payload)).toBe(payload);
40
+ });
41
+ });
42
+
43
+ describe("keyboard action controls", () => {
44
+ test("keeps string controls compatible", () => {
45
+ expect(getKeyboardControlBind("space")).toBe("space");
46
+ expect(resolveKeyboardActionInput("space", {}, {})).toEqual({
47
+ action: "action",
48
+ });
49
+ });
50
+
51
+ test("resolves object controls with static data", () => {
52
+ const control = {
53
+ bind: "space",
54
+ action: "projectile:shoot",
55
+ data: {
56
+ source: "keyboard",
57
+ target: { x: 10, y: 20 },
58
+ },
59
+ };
60
+
61
+ expect(getKeyboardControlBind(control)).toBe("space");
62
+ expect(resolveKeyboardActionInput(control, {}, {})).toEqual({
63
+ action: "projectile:shoot",
64
+ data: {
65
+ source: "keyboard",
66
+ target: { x: 10, y: 20 },
67
+ },
68
+ });
69
+ });
70
+
71
+ test("resolves object controls with functional data", () => {
72
+ const client = {
73
+ pointer: {
74
+ world: () => ({ x: 64, y: 96 }),
75
+ },
76
+ };
77
+ const sprite = { id: "player-1" };
78
+ const control = {
79
+ bind: "space",
80
+ action: "projectile:shoot",
81
+ data: (resolvedClient: typeof client, resolvedSprite: typeof sprite) => ({
82
+ source: "keyboard",
83
+ target: resolvedClient.pointer.world(),
84
+ playerId: resolvedSprite.id,
85
+ }),
86
+ };
87
+
88
+ expect(resolveKeyboardActionInput(control, client, sprite)).toEqual({
89
+ action: "projectile:shoot",
90
+ data: {
91
+ source: "keyboard",
92
+ target: { x: 64, y: 96 },
93
+ playerId: "player-1",
94
+ },
95
+ });
96
+ });
97
+
98
+ test("omits data when object controls do not provide it", () => {
99
+ expect(resolveKeyboardActionInput({
100
+ bind: "space",
101
+ action: "projectile:shoot",
102
+ }, {}, {})).toEqual({
103
+ action: "projectile:shoot",
104
+ });
105
+ });
106
+
107
+ test("matches keyboard events against string, numeric, and array binds", () => {
108
+ expect(
109
+ keyboardEventMatchesBind(
110
+ keyboardEvent({ key: " ", code: "Space", keyCode: 32 }),
111
+ "space"
112
+ )
113
+ ).toBe(true);
114
+ expect(
115
+ keyboardEventMatchesBind(
116
+ keyboardEvent({ key: "ArrowUp", code: "ArrowUp", keyCode: 38 }),
117
+ "up"
118
+ )
119
+ ).toBe(true);
120
+ expect(
121
+ keyboardEventMatchesBind(
122
+ keyboardEvent({ key: "x", code: "KeyX", keyCode: 88 }),
123
+ ["space", "x"]
124
+ )
125
+ ).toBe(true);
126
+ expect(
127
+ keyboardEventMatchesBind(
128
+ keyboardEvent({ key: "Escape", code: "Escape", keyCode: 27 }),
129
+ 27
130
+ )
131
+ ).toBe(true);
132
+ expect(
133
+ keyboardEventMatchesBind(
134
+ keyboardEvent({ key: "a", code: "KeyA", keyCode: 65 }),
135
+ "space"
136
+ )
137
+ ).toBe(false);
138
+ });
139
+
140
+ test("resolves directional keyboard controls from a native keyboard event", () => {
141
+ const controls = {
142
+ up: "up",
143
+ down: "down",
144
+ left: "left",
145
+ right: "right",
146
+ };
147
+
148
+ expect(
149
+ resolveKeyboardDirectionInput(
150
+ keyboardEvent({ key: "ArrowRight", code: "ArrowRight", keyCode: 39 }),
151
+ controls
152
+ )
153
+ ).toBe("right");
154
+ });
155
+ });
@@ -0,0 +1,120 @@
1
+ import { Direction, type RpgActionInput, type RpgActionName } from "@rpgjs/common";
2
+
3
+ export type KeyboardActionDataResolver<TClient = any, TSprite = any> = (
4
+ client: TClient,
5
+ sprite: TSprite,
6
+ ) => any;
7
+
8
+ export interface KeyboardActionConfig<TClient = any, TSprite = any> {
9
+ bind: any;
10
+ action?: RpgActionName;
11
+ data?: any | KeyboardActionDataResolver<TClient, TSprite>;
12
+ }
13
+
14
+ export function normalizeActionInput(action: RpgActionName, data?: any): RpgActionInput;
15
+ export function normalizeActionInput(action: RpgActionInput): RpgActionInput;
16
+ export function normalizeActionInput(action: RpgActionName | RpgActionInput, data?: any): RpgActionInput {
17
+ if (typeof action === "object") {
18
+ return action;
19
+ }
20
+ return data === undefined
21
+ ? { action }
22
+ : { action, data };
23
+ }
24
+
25
+ export function isKeyboardActionConfig(value: any): value is KeyboardActionConfig {
26
+ return value !== null
27
+ && typeof value === "object"
28
+ && Object.prototype.hasOwnProperty.call(value, "bind");
29
+ }
30
+
31
+ export function getKeyboardControlBind(control: any): any {
32
+ return isKeyboardActionConfig(control) ? control.bind : control;
33
+ }
34
+
35
+ const KEY_CODE_NAMES: Record<number, string> = {
36
+ 32: "space",
37
+ 27: "escape",
38
+ 37: "left",
39
+ 38: "up",
40
+ 39: "right",
41
+ 40: "down",
42
+ };
43
+
44
+ const normalizeKeyboardName = (value: unknown): string | undefined => {
45
+ if (typeof value !== "string") return undefined;
46
+ const normalized = value.toLowerCase();
47
+ if (
48
+ normalized === " " ||
49
+ normalized === "spacebar" ||
50
+ normalized === "space"
51
+ ) {
52
+ return "space";
53
+ }
54
+ if (normalized.startsWith("arrow")) {
55
+ return normalized.slice("arrow".length);
56
+ }
57
+ return normalized;
58
+ };
59
+
60
+ export function keyboardEventMatchesBind(
61
+ event: KeyboardEvent,
62
+ bind: any
63
+ ): boolean {
64
+ if (Array.isArray(bind)) {
65
+ return bind.some(item => keyboardEventMatchesBind(event, item));
66
+ }
67
+
68
+ if (typeof bind === "number") {
69
+ return event.keyCode === bind;
70
+ }
71
+
72
+ const expected = normalizeKeyboardName(bind);
73
+ if (!expected) return false;
74
+
75
+ return (
76
+ normalizeKeyboardName(event.key) === expected ||
77
+ normalizeKeyboardName(event.code) === expected ||
78
+ KEY_CODE_NAMES[event.keyCode] === expected
79
+ );
80
+ }
81
+
82
+ export function resolveKeyboardActionInput(
83
+ control: any,
84
+ client: any,
85
+ sprite: any,
86
+ defaultAction: RpgActionName = "action",
87
+ ): RpgActionInput {
88
+ if (!isKeyboardActionConfig(control)) {
89
+ return { action: defaultAction };
90
+ }
91
+
92
+ const action = control.action ?? defaultAction;
93
+ const data = typeof control.data === "function"
94
+ ? control.data(client, sprite)
95
+ : control.data;
96
+
97
+ return data === undefined
98
+ ? { action }
99
+ : { action, data };
100
+ }
101
+
102
+ export function resolveKeyboardDirectionInput(
103
+ event: KeyboardEvent,
104
+ keyboardControls: any
105
+ ): Direction | undefined {
106
+ const directions: Array<[any, Direction]> = [
107
+ [keyboardControls?.up, Direction.Up],
108
+ [keyboardControls?.down, Direction.Down],
109
+ [keyboardControls?.left, Direction.Left],
110
+ [keyboardControls?.right, Direction.Right],
111
+ ];
112
+
113
+ for (const [control, direction] of directions) {
114
+ if (keyboardEventMatchesBind(event, getKeyboardControlBind(control))) {
115
+ return direction;
116
+ }
117
+ }
118
+
119
+ return undefined;
120
+ }