@rpgjs/client 5.0.0-alpha.21 → 5.0.0-alpha.22

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 (72) hide show
  1. package/dist/Game/Object.d.ts +109 -0
  2. package/dist/RpgClientEngine.d.ts +111 -0
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/index10.js +1 -1
  7. package/dist/index15.js +42 -3
  8. package/dist/index15.js.map +1 -1
  9. package/dist/index19.js.map +1 -1
  10. package/dist/index2.js +142 -2
  11. package/dist/index2.js.map +1 -1
  12. package/dist/index20.js +15 -2
  13. package/dist/index20.js.map +1 -1
  14. package/dist/index23.js.map +1 -1
  15. package/dist/index26.js +2 -2
  16. package/dist/index26.js.map +1 -1
  17. package/dist/index27.js +2 -2
  18. package/dist/index27.js.map +1 -1
  19. package/dist/index34.js.map +1 -1
  20. package/dist/index35.js.map +1 -1
  21. package/dist/index36.js +8 -1
  22. package/dist/index36.js.map +1 -1
  23. package/dist/index37.js +1 -1
  24. package/dist/index38.js +1 -1
  25. package/dist/index40.js +6 -6
  26. package/dist/index41.js +320 -3681
  27. package/dist/index41.js.map +1 -1
  28. package/dist/index42.js +3686 -183
  29. package/dist/index42.js.map +1 -1
  30. package/dist/index43.js +71 -498
  31. package/dist/index43.js.map +1 -1
  32. package/dist/index44.js +182 -72
  33. package/dist/index44.js.map +1 -1
  34. package/dist/index45.js +500 -2
  35. package/dist/index45.js.map +1 -1
  36. package/dist/index46.js +3 -17
  37. package/dist/index46.js.map +1 -1
  38. package/dist/index47.js +16 -142
  39. package/dist/index47.js.map +1 -1
  40. package/dist/index48.js +206 -8
  41. package/dist/index48.js.map +1 -1
  42. package/dist/index49.js +7 -108
  43. package/dist/index49.js.map +1 -1
  44. package/dist/index50.js +104 -127
  45. package/dist/index50.js.map +1 -1
  46. package/dist/index51.js +122 -123
  47. package/dist/index51.js.map +1 -1
  48. package/dist/index52.js +123 -98
  49. package/dist/index52.js.map +1 -1
  50. package/dist/index53.js +107 -136
  51. package/dist/index53.js.map +1 -1
  52. package/dist/index54.js +139 -7
  53. package/dist/index54.js.map +1 -1
  54. package/dist/index55.js +7 -52
  55. package/dist/index55.js.map +1 -1
  56. package/dist/index56.js +54 -0
  57. package/dist/index56.js.map +1 -0
  58. package/dist/index8.js +8 -0
  59. package/dist/index8.js.map +1 -1
  60. package/dist/module.d.ts +43 -4
  61. package/dist/services/keyboardControls.d.ts +13 -2
  62. package/dist/services/mmorpg.d.ts +1 -1
  63. package/dist/services/standalone.d.ts +1 -1
  64. package/package.json +11 -10
  65. package/src/Game/Object.ts +82 -8
  66. package/src/RpgClientEngine.ts +173 -2
  67. package/src/components/character.ce +67 -4
  68. package/src/components/prebuilt/index.ts +1 -0
  69. package/src/components/scenes/draw-map.ce +12 -3
  70. package/src/index.ts +2 -1
  71. package/src/module.ts +56 -2
  72. package/src/services/keyboardControls.ts +13 -1
@@ -19,6 +19,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
19
19
  frames: { x: number; y: number; ts: number }[] = [];
20
20
  graphicsSignals = signal<any[]>([]);
21
21
  _component = {} // temporary component memory
22
+ flashTrigger = trigger();
22
23
 
23
24
  constructor() {
24
25
  super();
@@ -86,14 +87,87 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
86
87
 
87
88
  private animationSubscription?: Subscription;
88
89
 
89
- flash(color: string, duration: number = 100) {
90
- return new Promise((resolve) => {
91
- const lastTint = this.tint();
92
- this.tint.set(color);
93
- setTimeout(() => {
94
- this.tint.set(lastTint);
95
- resolve(true);
96
- }, duration);
90
+ /**
91
+ * Trigger a flash animation on this sprite
92
+ *
93
+ * This method triggers a flash effect using CanvasEngine's flash directive.
94
+ * The flash can be configured with various options including type (alpha, tint, or both),
95
+ * duration, cycles, and color.
96
+ *
97
+ * ## Design
98
+ *
99
+ * The flash uses a trigger system that is connected to the flash directive in the
100
+ * character component. This allows for flexible configuration and can be triggered
101
+ * from both server events and client-side code.
102
+ *
103
+ * @param options - Flash configuration options
104
+ * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')
105
+ * @param options.duration - Duration of the flash animation in milliseconds (default: 300)
106
+ * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)
107
+ * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)
108
+ * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Simple flash with default settings (alpha flash)
113
+ * player.flash();
114
+ *
115
+ * // Flash with red tint
116
+ * player.flash({ type: 'tint', tint: 0xff0000 });
117
+ *
118
+ * // Flash with both alpha and tint
119
+ * player.flash({
120
+ * type: 'both',
121
+ * alpha: 0.5,
122
+ * tint: 0xff0000,
123
+ * duration: 200,
124
+ * cycles: 2
125
+ * });
126
+ *
127
+ * // Quick damage flash
128
+ * player.flash({
129
+ * type: 'tint',
130
+ * tint: 0xff0000,
131
+ * duration: 150,
132
+ * cycles: 1
133
+ * });
134
+ * ```
135
+ */
136
+ flash(options?: {
137
+ type?: 'alpha' | 'tint' | 'both';
138
+ duration?: number;
139
+ cycles?: number;
140
+ alpha?: number;
141
+ tint?: number | string;
142
+ }): void {
143
+ const flashOptions = {
144
+ type: options?.type || 'alpha',
145
+ duration: options?.duration ?? 300,
146
+ cycles: options?.cycles ?? 1,
147
+ alpha: options?.alpha ?? 0.3,
148
+ tint: options?.tint ?? 0xffffff,
149
+ };
150
+
151
+ // Convert color name to hex if needed
152
+ let tintValue = flashOptions.tint;
153
+ if (typeof tintValue === 'string') {
154
+ // Common color name to hex mapping
155
+ const colorMap: Record<string, number> = {
156
+ 'white': 0xffffff,
157
+ 'red': 0xff0000,
158
+ 'green': 0x00ff00,
159
+ 'blue': 0x0000ff,
160
+ 'yellow': 0xffff00,
161
+ 'cyan': 0x00ffff,
162
+ 'magenta': 0xff00ff,
163
+ 'black': 0x000000,
164
+ };
165
+ tintValue = colorMap[tintValue.toLowerCase()] ?? 0xffffff;
166
+ }
167
+
168
+ this.flashTrigger.start({
169
+ ...flashOptions,
170
+ tint: tintValue,
97
171
  });
98
172
  }
99
173
 
@@ -1,6 +1,6 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
2
  import { Context, inject } from "@signe/di";
3
- import { signal, bootstrapCanvas, Howler, Howl } from "canvasengine";
3
+ import { signal, bootstrapCanvas, KeyboardControls, Howl, trigger } from "canvasengine";
4
4
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
5
5
  import { LoadMapService, LoadMapToken } from "./services/loadMap";
6
6
  import { RpgSound } from "./Sound";
@@ -20,7 +20,6 @@ import {
20
20
  PredictionController,
21
21
  type PredictionState,
22
22
  } from "@rpgjs/common";
23
- import { KeyboardControls } from "./services/keyboardControls";
24
23
 
25
24
  export class RpgClientEngine<T = any> {
26
25
  private guiService: RpgGui;
@@ -49,6 +48,10 @@ export class RpgClientEngine<T = any> {
49
48
  playerIdSignal = signal<string | null>(null);
50
49
  spriteComponentsBehind = signal<any[]>([]);
51
50
  spriteComponentsInFront = signal<any[]>([]);
51
+ /** ID of the sprite that the camera should follow. null means follow the current player */
52
+ cameraFollowTargetId = signal<string | null>(null);
53
+ /** Trigger for map shake animation */
54
+ mapShakeTrigger = trigger();
52
55
 
53
56
  private predictionEnabled = false;
54
57
  private prediction?: PredictionController<Direction>;
@@ -214,6 +217,8 @@ export class RpgClientEngine<T = any> {
214
217
 
215
218
  this.webSocket.on("changeMap", (data) => {
216
219
  this.sceneMap.reset()
220
+ // Reset camera follow to default (follow current player) when changing maps
221
+ this.cameraFollowTargetId.set(null);
217
222
  this.loadScene(data.mapId);
218
223
  });
219
224
 
@@ -242,6 +247,33 @@ export class RpgClientEngine<T = any> {
242
247
  this.stopSound(soundId);
243
248
  });
244
249
 
250
+ this.webSocket.on("stopAllSounds", () => {
251
+ this.stopAllSounds();
252
+ });
253
+
254
+ this.webSocket.on("cameraFollow", (data) => {
255
+ const { targetId, smoothMove } = data;
256
+ this.setCameraFollow(targetId, smoothMove);
257
+ });
258
+
259
+ this.webSocket.on("flash", (data) => {
260
+ const { object, type, duration, cycles, alpha, tint } = data;
261
+ const sprite = object ? this.sceneMap.getObjectById(object) : undefined;
262
+ if (sprite && typeof sprite.flash === 'function') {
263
+ sprite.flash({ type, duration, cycles, alpha, tint });
264
+ }
265
+ });
266
+
267
+ this.webSocket.on("shakeMap", (data) => {
268
+ const { intensity, duration, frequency, direction } = data || {};
269
+ this.mapShakeTrigger.start({
270
+ intensity,
271
+ duration,
272
+ frequency,
273
+ direction
274
+ });
275
+ });
276
+
245
277
  this.webSocket.on('open', () => {
246
278
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
247
279
  // Start ping/pong for synchronization
@@ -673,6 +705,79 @@ export class RpgClientEngine<T = any> {
673
705
  }
674
706
  }
675
707
 
708
+ /**
709
+ * Stop all currently playing sounds
710
+ *
711
+ * This method stops all sounds that are currently playing.
712
+ * Useful when changing maps to prevent sound overlap.
713
+ *
714
+ * @example
715
+ * ```ts
716
+ * // Stop all sounds
717
+ * engine.stopAllSounds();
718
+ * ```
719
+ */
720
+ stopAllSounds(): void {
721
+ this.sounds.forEach((sound) => {
722
+ if (sound && sound.stop) {
723
+ sound.stop();
724
+ }
725
+ });
726
+ }
727
+
728
+ /**
729
+ * Set the camera to follow a specific sprite
730
+ *
731
+ * This method changes which sprite the camera viewport should follow.
732
+ * The camera will smoothly animate to the target sprite if smoothMove options are provided.
733
+ *
734
+ * ## Design
735
+ *
736
+ * The camera follow target is stored in a signal that is read by sprite components.
737
+ * Each sprite checks if it should be followed by comparing its ID with the target ID.
738
+ * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's
739
+ * viewport system.
740
+ *
741
+ * @param targetId - The ID of the sprite to follow. Set to null to follow the current player
742
+ * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease
743
+ * @param smoothMove.time - Duration of the animation in milliseconds (optional)
744
+ * @param smoothMove.ease - Easing function name from https://easings.net (optional)
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * // Follow another player with default smooth animation
749
+ * engine.setCameraFollow(otherPlayerId, true);
750
+ *
751
+ * // Follow an event with custom smooth animation
752
+ * engine.setCameraFollow(eventId, {
753
+ * time: 1000,
754
+ * ease: "easeInOutQuad"
755
+ * });
756
+ *
757
+ * // Follow without animation (instant)
758
+ * engine.setCameraFollow(targetId, false);
759
+ *
760
+ * // Return to following current player
761
+ * engine.setCameraFollow(null);
762
+ * ```
763
+ */
764
+ setCameraFollow(
765
+ targetId: string | null,
766
+ smoothMove?: boolean | { time?: number; ease?: string }
767
+ ): void {
768
+ // Store smoothMove options for potential future use with viewport animation
769
+ // For now, we just set the target ID and let CanvasEngine handle the viewport follow
770
+ // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it
771
+ this.cameraFollowTargetId.set(targetId);
772
+
773
+ // If smoothMove is an object, we could store it for viewport configuration
774
+ // This would require integration with CanvasEngine's viewport animation system
775
+ if (typeof smoothMove === "object" && smoothMove !== null) {
776
+ // Future: Apply smoothMove.time and smoothMove.ease to viewport animation
777
+ // For now, CanvasEngine handles viewport following automatically
778
+ }
779
+ }
780
+
676
781
  addParticle(particle: any) {
677
782
  this.particleSettings.emitters.push(particle)
678
783
  return particle;
@@ -900,6 +1005,72 @@ export class RpgClientEngine<T = any> {
900
1005
  this.inputFrameCounter = 0;
901
1006
  }
902
1007
 
1008
+ /**
1009
+ * Trigger a flash animation on a sprite
1010
+ *
1011
+ * This method allows you to trigger a flash effect on any sprite from client-side code.
1012
+ * The flash can be configured with various options including type (alpha, tint, or both),
1013
+ * duration, cycles, and color.
1014
+ *
1015
+ * ## Design
1016
+ *
1017
+ * The flash is applied directly to the sprite object using its flash trigger.
1018
+ * This is useful for client-side visual feedback, UI interactions, or local effects
1019
+ * that don't need to be synchronized with the server.
1020
+ *
1021
+ * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player
1022
+ * @param options - Flash configuration options
1023
+ * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')
1024
+ * @param options.duration - Duration of the flash animation in milliseconds (default: 300)
1025
+ * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)
1026
+ * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)
1027
+ * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)
1028
+ *
1029
+ * @example
1030
+ * ```ts
1031
+ * // Flash the current player with default settings
1032
+ * engine.flash();
1033
+ *
1034
+ * // Flash a specific sprite with red tint
1035
+ * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });
1036
+ *
1037
+ * // Flash with both alpha and tint for dramatic effect
1038
+ * engine.flash(undefined, {
1039
+ * type: 'both',
1040
+ * alpha: 0.5,
1041
+ * tint: 0xff0000,
1042
+ * duration: 200,
1043
+ * cycles: 2
1044
+ * });
1045
+ *
1046
+ * // Quick damage flash on current player
1047
+ * engine.flash(undefined, {
1048
+ * type: 'tint',
1049
+ * tint: 'red',
1050
+ * duration: 150,
1051
+ * cycles: 1
1052
+ * });
1053
+ * ```
1054
+ */
1055
+ flash(
1056
+ spriteId?: string,
1057
+ options?: {
1058
+ type?: 'alpha' | 'tint' | 'both';
1059
+ duration?: number;
1060
+ cycles?: number;
1061
+ alpha?: number;
1062
+ tint?: number | string;
1063
+ }
1064
+ ): void {
1065
+ const targetId = spriteId || this.playerId;
1066
+ if (!targetId) return;
1067
+
1068
+ const sprite = this.sceneMap.getObjectById(targetId);
1069
+ if (sprite && typeof sprite.flash === 'function') {
1070
+ sprite.flash(options);
1071
+ }
1072
+ }
1073
+
903
1074
  private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
904
1075
  if (this.predictionEnabled && this.prediction) {
905
1076
  this.prediction.applyServerAck({
@@ -1,4 +1,4 @@
1
- <Container x={smoothX} y={smoothY} zIndex={y} viewportFollow={isMe} controls onBeforeDestroy visible>
1
+ <Container x={smoothX} y={smoothY} zIndex={y} viewportFollow={shouldFollowCamera} controls onBeforeDestroy visible >
2
2
  @for (component of componentsBehind) {
3
3
  <Container>
4
4
  <component object />
@@ -7,7 +7,7 @@
7
7
  <Particle emit={@emitParticleTrigger} settings={@particleSettings} zIndex={1000} name={particleName} />
8
8
  <Container>
9
9
  @for (graphicObj of graphicsSignals) {
10
- <Sprite sheet={@sheet(@graphicObj)} direction tint hitbox />
10
+ <Sprite sheet={@sheet(@graphicObj)} direction tint hitbox flash={flashConfig} />
11
11
  }
12
12
  </Container>
13
13
  @for (component of componentsInFront) {
@@ -29,7 +29,8 @@
29
29
  </Container>
30
30
 
31
31
  <script>
32
- import { signal, effect, mount, computed, tick, animatedSignal } from "canvasengine";
32
+ import { signal, effect, mount, computed, tick, animatedSignal, on } from "canvasengine";
33
+
33
34
  import { lastValueFrom, combineLatest, pairwise, filter, map, startWith } from "rxjs";
34
35
  import { Particle } from "@canvasengine/presets";
35
36
  import { GameEngineToken, ModulesToken } from "@rpgjs/common";
@@ -51,6 +52,23 @@
51
52
  const componentsBehind = client.spriteComponentsBehind;
52
53
  const componentsInFront = client.spriteComponentsInFront;
53
54
  const isMe = computed(() => id() === playerId);
55
+
56
+ /**
57
+ * Determine if the camera should follow this sprite
58
+ *
59
+ * The camera follows this sprite if:
60
+ * - It's explicitly set as the camera follow target, OR
61
+ * - It's the current player and no explicit target is set (default behavior)
62
+ */
63
+ const shouldFollowCamera = computed(() => {
64
+ const cameraTargetId = client.cameraFollowTargetId();
65
+ // If a target is explicitly set, only follow if this sprite is the target
66
+ if (cameraTargetId !== null) {
67
+ return id() === cameraTargetId;
68
+ }
69
+ // Otherwise, follow the current player (default behavior)
70
+ return isMe();
71
+ });
54
72
 
55
73
  /**
56
74
  * Get all attached GUI components that should be rendered on sprites
@@ -80,9 +98,51 @@
80
98
  graphics,
81
99
  hitbox,
82
100
  isConnected,
83
- graphicsSignals
101
+ graphicsSignals,
102
+ flashTrigger
84
103
  } = object;
85
104
 
105
+ /**
106
+ * Flash configuration signals for dynamic options
107
+ * These signals are updated when the flash trigger is activated with options
108
+ */
109
+ const flashType = signal<'alpha' | 'tint' | 'both'>('alpha');
110
+ const flashDuration = signal(300);
111
+ const flashCycles = signal(1);
112
+ const flashAlpha = signal(0.3);
113
+ const flashTint = signal(0xffffff);
114
+
115
+ /**
116
+ * Listen to flash trigger to update configuration dynamically
117
+ * When flash is triggered with options, update the signals
118
+ */
119
+ on(flashTrigger, (data) => {
120
+ if (data && typeof data === 'object') {
121
+ if (data.type !== undefined) flashType.set(data.type);
122
+ if (data.duration !== undefined) flashDuration.set(data.duration);
123
+ if (data.cycles !== undefined) flashCycles.set(data.cycles);
124
+ if (data.alpha !== undefined) flashAlpha.set(data.alpha);
125
+ if (data.tint !== undefined) flashTint.set(data.tint);
126
+ }
127
+ });
128
+
129
+ /**
130
+ * Flash configuration for the sprite
131
+ *
132
+ * This configuration is used by the flash directive to create visual feedback effects.
133
+ * The flash trigger is exposed through the object, allowing both server events and
134
+ * client-side code to trigger flash animations. Options can be passed dynamically
135
+ * through the trigger, which updates the reactive signals.
136
+ */
137
+ const flashConfig = computed(() => ({
138
+ trigger: flashTrigger,
139
+ type: flashType(),
140
+ duration: flashDuration(),
141
+ cycles: flashCycles(),
142
+ alpha: flashAlpha(),
143
+ tint: flashTint(),
144
+ }));
145
+
86
146
  const particleSettings = client.particleSettings;
87
147
 
88
148
  const canControls = () => isMe() && object.canMove()
@@ -135,6 +195,9 @@
135
195
  }
136
196
  },
137
197
  },
198
+ gamepad: {
199
+ enabled: true
200
+ }
138
201
  });
139
202
 
140
203
  const smoothX = animatedSignal(x(), {
@@ -20,3 +20,4 @@ export { default as HpBar } from './hp-bar.ce';
20
20
 
21
21
 
22
22
 
23
+
@@ -1,4 +1,4 @@
1
- <Container sound={backgroundMusic} >
1
+ <Container sound={backgroundMusic} shake={shakeConfig}>
2
2
  <Container sound={backgroundAmbientSound} />
3
3
 
4
4
  <sceneComponent() data={map.@data} params={map.@params} />
@@ -13,7 +13,8 @@
13
13
  </Container>
14
14
 
15
15
  <script>
16
- import { effect, signal, computed, mount } from 'canvasengine'
16
+ import { NoiseFilter } from 'pixi-filters';
17
+ import { effect, signal, computed, mount, on, tick } from 'canvasengine'
17
18
  import { inject } from "../../core/inject";
18
19
  import { RpgClientEngine } from "../../RpgClientEngine";
19
20
 
@@ -26,7 +27,7 @@
26
27
  const animations = engine.sceneMap.animations
27
28
  const backgroundMusic = { src: mapParams?.backgroundMusic, autoplay: true, loop: true }
28
29
  const backgroundAmbientSound = { src: mapParams?.backgroundAmbientSound, autoplay: true, loop: true }
29
-
30
+
30
31
  const data = signal(map().data)
31
32
 
32
33
  const scale = mapParams?.scale || 1
@@ -36,4 +37,12 @@
36
37
  const clamp = {
37
38
  direction: "all"
38
39
  }
40
+
41
+ const shakeConfig = {
42
+ trigger: engine.mapShakeTrigger,
43
+ intensity: 10,
44
+ duration: 500,
45
+ frequency: 10,
46
+ direction: 'both'
47
+ }
39
48
  </script>
package/src/index.ts CHANGED
@@ -15,4 +15,5 @@ export * from "./components/gui";
15
15
  export * from "./Sound";
16
16
  export * from "./Resource";
17
17
  export { Context } from "@signe/di";
18
- export * from "./services/keyboardControls";
18
+ export { KeyboardControls, Input } from "canvasengine";
19
+ export { Control } from "./services/keyboardControls";
package/src/module.ts CHANGED
@@ -1,15 +1,69 @@
1
1
  import { findModules, provideModules } from "@rpgjs/common";
2
+ import { FactoryProvider } from "@signe/di";
2
3
  import { RpgClientEngine } from "./RpgClientEngine";
3
4
  import { RpgClient } from "./RpgClient";
4
5
  import { inject } from "@signe/di";
5
6
  import { RpgGui } from "./Gui/Gui";
6
7
  import { getSoundMetadata } from "./Sound";
7
8
 
8
- export function provideClientModules(modules: RpgClient[]) {
9
+ /**
10
+ * Type for client modules that can be either:
11
+ * - An object implementing RpgClient interface
12
+ * - A class decorated with @RpgModule decorator
13
+ */
14
+ export type RpgClientModule = RpgClient | (new () => any);
15
+
16
+ /**
17
+ * Provides client modules configuration to Dependency Injection
18
+ *
19
+ * This function accepts an array of client modules that can be either:
20
+ * - Objects implementing the RpgClient interface
21
+ * - Classes decorated with the @RpgModule decorator (which will be instantiated)
22
+ *
23
+ * @param modules - Array of client modules (objects or classes)
24
+ * @returns FactoryProvider configuration for DI
25
+ * @example
26
+ * ```ts
27
+ * // Using an object
28
+ * provideClientModules([
29
+ * {
30
+ * engine: {
31
+ * onConnected(engine) {
32
+ * console.log('Client connected')
33
+ * }
34
+ * }
35
+ * }
36
+ * ])
37
+ *
38
+ * // Using a decorated class
39
+ * @RpgModule<RpgClient>({
40
+ * engine: {
41
+ * onStart(engine) {
42
+ * console.log('Client started')
43
+ * }
44
+ * }
45
+ * })
46
+ * class MyClientModule {}
47
+ *
48
+ * provideClientModules([MyClientModule])
49
+ * ```
50
+ */
51
+ export function provideClientModules(modules: RpgClientModule[]): FactoryProvider {
9
52
  return provideModules(modules, "client", (modules, context) => {
10
53
  const mainModuleClient = findModules(context, 'Client')
11
54
  modules = [...mainModuleClient, ...modules]
12
55
  modules = modules.map((module) => {
56
+ // If module is a class (constructor function), instantiate it
57
+ // The RpgModule decorator adds properties to the prototype, which will be accessible via the instance
58
+ if (typeof module === 'function') {
59
+ const instance = new module() as any;
60
+ // Copy all enumerable properties (including from prototype) to a plain object
61
+ const moduleObj: any = {};
62
+ for (const key in instance) {
63
+ moduleObj[key] = instance[key];
64
+ }
65
+ module = moduleObj;
66
+ }
13
67
  if ('client' in module) {
14
68
  module = module.client as any;
15
69
  }
@@ -84,7 +138,7 @@ export function provideClientModules(modules: RpgClient[]) {
84
138
  const gui = [...module.gui];
85
139
  module.gui = {
86
140
  load: (engine: RpgClientEngine) => {
87
- const guiService = inject(engine.context, RpgGui);
141
+ const guiService = inject(engine.context, RpgGui) as RpgGui;
88
142
  gui.forEach((gui) => {
89
143
  guiService.add(gui);
90
144
  });
@@ -1,4 +1,16 @@
1
- export const KeyboardControls = "KeyboardControlsToken";
1
+ import { KeyboardControls } from "canvasengine";
2
+
3
+ export enum Control {
4
+ Action = 'action',
5
+ Attack = 'attack',
6
+ Defense = 'defense',
7
+ Skill = 'skill',
8
+ Back = 'back',
9
+ Up = 1,
10
+ Down = 3,
11
+ Right = 2,
12
+ Left = 4
13
+ }
2
14
 
3
15
  export function provideKeyboardControls() {
4
16
  return {