@rpgjs/client 5.0.0-beta.1 → 5.0.0-beta.11

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 (245) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +19 -0
  3. package/dist/Game/AnimationManager.d.ts +1 -1
  4. package/dist/Game/AnimationManager.js +18 -9
  5. package/dist/Game/AnimationManager.js.map +1 -1
  6. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  7. package/dist/Game/Event.js.map +1 -1
  8. package/dist/Game/Map.d.ts +9 -1
  9. package/dist/Game/Map.js +63 -5
  10. package/dist/Game/Map.js.map +1 -1
  11. package/dist/Game/Object.d.ts +47 -15
  12. package/dist/Game/Object.js +82 -38
  13. package/dist/Game/Object.js.map +1 -1
  14. package/dist/Game/Player.js.map +1 -1
  15. package/dist/Game/ProjectileManager.d.ts +89 -0
  16. package/dist/Game/ProjectileManager.js +179 -0
  17. package/dist/Game/ProjectileManager.js.map +1 -0
  18. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  19. package/dist/Gui/Gui.d.ts +17 -4
  20. package/dist/Gui/Gui.js +78 -48
  21. package/dist/Gui/Gui.js.map +1 -1
  22. package/dist/Gui/Gui.spec.d.ts +1 -0
  23. package/dist/Gui/NotificationManager.js.map +1 -1
  24. package/dist/Resource.js +1 -1
  25. package/dist/Resource.js.map +1 -1
  26. package/dist/RpgClient.d.ts +110 -15
  27. package/dist/RpgClientEngine.d.ts +86 -10
  28. package/dist/RpgClientEngine.js +306 -49
  29. package/dist/RpgClientEngine.js.map +1 -1
  30. package/dist/Sound.js.map +1 -1
  31. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorate.js +1 -1
  32. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorateMetadata.js +1 -1
  33. package/dist/components/animations/animation.ce.js +4 -5
  34. package/dist/components/animations/animation.ce.js.map +1 -1
  35. package/dist/components/animations/hit.ce.js +19 -25
  36. package/dist/components/animations/hit.ce.js.map +1 -1
  37. package/dist/components/animations/index.js +4 -4
  38. package/dist/components/animations/index.js.map +1 -1
  39. package/dist/components/character.ce.js +422 -240
  40. package/dist/components/character.ce.js.map +1 -1
  41. package/dist/components/dynamics/bar.ce.js +97 -0
  42. package/dist/components/dynamics/bar.ce.js.map +1 -0
  43. package/dist/components/dynamics/image.ce.js +24 -0
  44. package/dist/components/dynamics/image.ce.js.map +1 -0
  45. package/dist/components/dynamics/parse-value.d.ts +3 -0
  46. package/dist/components/dynamics/parse-value.js +54 -35
  47. package/dist/components/dynamics/parse-value.js.map +1 -1
  48. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  49. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  50. package/dist/components/dynamics/shape-utils.js +73 -0
  51. package/dist/components/dynamics/shape-utils.js.map +1 -0
  52. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  53. package/dist/components/dynamics/shape.ce.js +84 -0
  54. package/dist/components/dynamics/shape.ce.js.map +1 -0
  55. package/dist/components/dynamics/text.ce.js +34 -56
  56. package/dist/components/dynamics/text.ce.js.map +1 -1
  57. package/dist/components/gui/box.ce.js +6 -8
  58. package/dist/components/gui/box.ce.js.map +1 -1
  59. package/dist/components/gui/dialogbox/index.ce.js +56 -62
  60. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  61. package/dist/components/gui/gameover.ce.js +42 -65
  62. package/dist/components/gui/gameover.ce.js.map +1 -1
  63. package/dist/components/gui/hud/hud.ce.js +21 -30
  64. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  65. package/dist/components/gui/menu/equip-menu.ce.js +112 -165
  66. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  67. package/dist/components/gui/menu/exit-menu.ce.js +8 -6
  68. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  69. package/dist/components/gui/menu/items-menu.ce.js +52 -69
  70. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  71. package/dist/components/gui/menu/main-menu.ce.js +75 -92
  72. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  73. package/dist/components/gui/menu/options-menu.ce.js +5 -4
  74. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  75. package/dist/components/gui/menu/skills-menu.ce.js +12 -17
  76. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  77. package/dist/components/gui/mobile/index.js +2 -2
  78. package/dist/components/gui/mobile/index.js.map +1 -1
  79. package/dist/components/gui/mobile/mobile.ce.js +5 -4
  80. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  81. package/dist/components/gui/notification/notification.ce.js +22 -24
  82. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  83. package/dist/components/gui/save-load.ce.js +72 -249
  84. package/dist/components/gui/save-load.ce.js.map +1 -1
  85. package/dist/components/gui/shop/shop.ce.js +90 -127
  86. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  87. package/dist/components/gui/title-screen.ce.js +45 -70
  88. package/dist/components/gui/title-screen.ce.js.map +1 -1
  89. package/dist/components/index.d.ts +2 -1
  90. package/dist/components/index.js +1 -0
  91. package/dist/components/player-components-utils.d.ts +67 -0
  92. package/dist/components/player-components-utils.js +162 -0
  93. package/dist/components/player-components-utils.js.map +1 -0
  94. package/dist/components/player-components-utils.spec.d.ts +1 -0
  95. package/dist/components/player-components.ce.js +189 -0
  96. package/dist/components/player-components.ce.js.map +1 -0
  97. package/dist/components/prebuilt/hp-bar.ce.js +42 -44
  98. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  99. package/dist/components/prebuilt/light-halo.ce.js +36 -59
  100. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  101. package/dist/components/scenes/canvas.ce.js +165 -21
  102. package/dist/components/scenes/canvas.ce.js.map +1 -1
  103. package/dist/components/scenes/draw-map.ce.js +25 -32
  104. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  105. package/dist/components/scenes/event-layer.ce.js +9 -8
  106. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  107. package/dist/core/inject.js +1 -1
  108. package/dist/core/inject.js.map +1 -1
  109. package/dist/core/setup.js +1 -1
  110. package/dist/core/setup.js.map +1 -1
  111. package/dist/decorators/spritesheet.d.ts +1 -0
  112. package/dist/decorators/spritesheet.js +11 -0
  113. package/dist/decorators/spritesheet.js.map +1 -0
  114. package/dist/index.d.ts +4 -0
  115. package/dist/index.js +26 -21
  116. package/dist/module.js +15 -1
  117. package/dist/module.js.map +1 -1
  118. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.0.1}/node_modules/@signe/di/dist/index.js +7 -117
  119. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
  120. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js +239 -0
  121. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
  122. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  123. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  124. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js +696 -0
  125. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
  126. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js +44 -0
  127. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  128. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/index.js +57 -141
  129. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
  130. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  131. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  132. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +27 -27
  133. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  134. package/dist/presets/animation.js.map +1 -1
  135. package/dist/presets/faceset.js.map +1 -1
  136. package/dist/presets/icon.js.map +1 -1
  137. package/dist/presets/index.js.map +1 -1
  138. package/dist/presets/lpc.js.map +1 -1
  139. package/dist/presets/rmspritesheet.js.map +1 -1
  140. package/dist/services/AbstractSocket.js.map +1 -1
  141. package/dist/services/actionInput.d.ts +12 -0
  142. package/dist/services/actionInput.js +27 -0
  143. package/dist/services/actionInput.js.map +1 -0
  144. package/dist/services/actionInput.spec.d.ts +1 -0
  145. package/dist/services/keyboardControls.js.map +1 -1
  146. package/dist/services/loadMap.d.ts +6 -0
  147. package/dist/services/loadMap.js +1 -1
  148. package/dist/services/loadMap.js.map +1 -1
  149. package/dist/services/mmorpg-connection.d.ts +5 -0
  150. package/dist/services/mmorpg-connection.js +50 -0
  151. package/dist/services/mmorpg-connection.js.map +1 -0
  152. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  153. package/dist/services/mmorpg.d.ts +10 -4
  154. package/dist/services/mmorpg.js +56 -33
  155. package/dist/services/mmorpg.js.map +1 -1
  156. package/dist/services/pointerContext.d.ts +11 -0
  157. package/dist/services/pointerContext.js +48 -0
  158. package/dist/services/pointerContext.js.map +1 -0
  159. package/dist/services/pointerContext.spec.d.ts +1 -0
  160. package/dist/services/save.js.map +1 -1
  161. package/dist/services/save.spec.d.ts +1 -0
  162. package/dist/services/standalone-message.d.ts +1 -0
  163. package/dist/services/standalone-message.js +9 -0
  164. package/dist/services/standalone-message.js.map +1 -0
  165. package/dist/services/standalone.js +4 -3
  166. package/dist/services/standalone.js.map +1 -1
  167. package/dist/services/standalone.spec.d.ts +1 -0
  168. package/dist/utils/getEntityProp.js +4 -3
  169. package/dist/utils/getEntityProp.js.map +1 -1
  170. package/dist/utils/getEntityProp.spec.d.ts +1 -0
  171. package/dist/utils/readPropValue.d.ts +2 -0
  172. package/dist/utils/readPropValue.js +13 -0
  173. package/dist/utils/readPropValue.js.map +1 -0
  174. package/package.json +13 -14
  175. package/src/Game/AnimationManager.spec.ts +30 -0
  176. package/src/Game/AnimationManager.ts +22 -10
  177. package/src/Game/Map.ts +91 -2
  178. package/src/Game/Object.ts +148 -69
  179. package/src/Game/ProjectileManager.spec.ts +338 -0
  180. package/src/Game/ProjectileManager.ts +324 -0
  181. package/src/Gui/Gui.spec.ts +273 -0
  182. package/src/Gui/Gui.ts +105 -50
  183. package/src/Resource.ts +1 -2
  184. package/src/RpgClient.ts +125 -17
  185. package/src/RpgClientEngine.ts +457 -87
  186. package/src/components/character.ce +441 -32
  187. package/src/components/dynamics/bar.ce +88 -0
  188. package/src/components/dynamics/image.ce +21 -0
  189. package/src/components/dynamics/parse-value.spec.ts +83 -0
  190. package/src/components/dynamics/parse-value.ts +111 -37
  191. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  192. package/src/components/dynamics/shape-utils.ts +61 -0
  193. package/src/components/dynamics/shape.ce +90 -0
  194. package/src/components/dynamics/text.ce +35 -149
  195. package/src/components/gui/dialogbox/index.ce +18 -8
  196. package/src/components/gui/gameover.ce +2 -1
  197. package/src/components/gui/menu/equip-menu.ce +2 -1
  198. package/src/components/gui/menu/exit-menu.ce +2 -1
  199. package/src/components/gui/menu/items-menu.ce +3 -2
  200. package/src/components/gui/menu/main-menu.ce +2 -1
  201. package/src/components/gui/save-load.ce +2 -1
  202. package/src/components/gui/shop/shop.ce +3 -2
  203. package/src/components/gui/title-screen.ce +2 -1
  204. package/src/components/index.ts +2 -1
  205. package/src/components/player-components-utils.spec.ts +109 -0
  206. package/src/components/player-components-utils.ts +205 -0
  207. package/src/components/player-components.ce +222 -0
  208. package/src/components/prebuilt/hp-bar.ce +4 -3
  209. package/src/components/prebuilt/light-halo.ce +2 -2
  210. package/src/components/scenes/canvas.ce +175 -8
  211. package/src/components/scenes/draw-map.ce +18 -17
  212. package/src/components/scenes/event-layer.ce +1 -2
  213. package/src/core/setup.ts +2 -2
  214. package/src/decorators/spritesheet.ts +8 -0
  215. package/src/index.ts +4 -0
  216. package/src/module.ts +18 -1
  217. package/src/services/actionInput.spec.ts +101 -0
  218. package/src/services/actionInput.ts +53 -0
  219. package/src/services/loadMap.ts +2 -0
  220. package/src/services/mmorpg-connection.spec.ts +99 -0
  221. package/src/services/mmorpg-connection.ts +69 -0
  222. package/src/services/mmorpg.ts +68 -36
  223. package/src/services/pointerContext.spec.ts +36 -0
  224. package/src/services/pointerContext.ts +84 -0
  225. package/src/services/save.spec.ts +127 -0
  226. package/src/services/standalone-message.ts +7 -0
  227. package/src/services/standalone.spec.ts +34 -0
  228. package/src/services/standalone.ts +3 -2
  229. package/src/utils/getEntityProp.spec.ts +96 -0
  230. package/src/utils/getEntityProp.ts +4 -3
  231. package/src/utils/readPropValue.ts +16 -0
  232. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  233. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
  234. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
  235. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  236. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  237. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  238. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  239. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  240. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  241. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  242. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  243. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  244. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  245. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
@@ -1,15 +1,33 @@
1
1
  import { Hooks, ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
2
- import { trigger, signal, effect } from "canvasengine";
3
- import { filter, from, map, 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";
6
+ type Frame = { x: number; y: number; ts: number };
7
7
 
8
- const DYNAMIC_COMPONENTS = {
9
- text: TextComponent,
10
- }
8
+ type AnimationRestoreOptions = {
9
+ restoreAnimationName?: string;
10
+ restoreGraphics?: any[];
11
+ timeoutMs?: number;
12
+ };
11
13
 
12
- type Frame = { x: number; y: number; ts: number };
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>;
30
+ };
13
31
 
14
32
  export abstract class RpgClientObject extends RpgCommonPlayer {
15
33
  abstract _type: string;
@@ -20,8 +38,11 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
20
38
  _param = signal({});
21
39
  frames: Frame[] = [];
22
40
  graphicsSignals = signal<any[]>([]);
23
- _component = {} // temporary component memory
24
- flashTrigger = trigger();
41
+ flashTrigger: ConfigurableTrigger<FlashTriggerOptions> = trigger<FlashTriggerOptions>();
42
+ private animationRestoreState?: {
43
+ animationName: string;
44
+ graphics: any[];
45
+ };
25
46
 
26
47
  constructor() {
27
48
  super();
@@ -39,31 +60,15 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
39
60
  this.graphics.observable
40
61
  .pipe(
41
62
  map(({ items }) => items),
42
- filter(graphics => graphics.length > 0),
43
- switchMap(graphics => from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic)))))
63
+ switchMap(graphics => {
64
+ if (graphics.length === 0) return of([]);
65
+ return from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic))));
66
+ })
44
67
  )
45
68
  .subscribe((sheets) => {
46
69
  this.graphicsSignals.set(sheets);
47
70
  });
48
71
 
49
- this.componentsTop.observable
50
- .pipe(
51
- filter(value => value !== null && value !== undefined),
52
- map((value) => typeof value === 'string' ? JSON.parse(value) : value),
53
- )
54
- .subscribe(({components}) => {
55
- for (const component of components) {
56
- for (const [key, value] of Object.entries(component)) {
57
- this._component = value as any; // temporary component memory
58
- console.log(value)
59
- const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;
60
- if (DYNAMIC_COMPONENTS[type]) {
61
- this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);
62
- }
63
- }
64
- }
65
- });
66
-
67
72
  this.engine.tick
68
73
  .pipe
69
74
  //throttleTime(10)
@@ -101,6 +106,38 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
101
106
  }
102
107
 
103
108
  private animationSubscription?: Subscription;
109
+ private animationResetTimeout?: ReturnType<typeof setTimeout>;
110
+ private animationWaitResolve?: () => void;
111
+
112
+ private clearAnimationControls() {
113
+ if (this.animationSubscription) {
114
+ this.animationSubscription.unsubscribe();
115
+ this.animationSubscription = undefined;
116
+ }
117
+ if (this.animationResetTimeout) {
118
+ clearTimeout(this.animationResetTimeout);
119
+ this.animationResetTimeout = undefined;
120
+ }
121
+ }
122
+
123
+ private resolveAnimationWait() {
124
+ const resolve = this.animationWaitResolve;
125
+ this.animationWaitResolve = undefined;
126
+ resolve?.();
127
+ }
128
+
129
+ private finishTemporaryAnimation() {
130
+ const restoreState = this.animationRestoreState;
131
+ this.clearAnimationControls();
132
+ this.animationCurrentIndex.set(0);
133
+ if (restoreState) {
134
+ this.animationName.set(restoreState.animationName);
135
+ this.graphics.set([...restoreState.graphics]);
136
+ }
137
+ this.animationRestoreState = undefined;
138
+ this.animationIsPlaying.set(false);
139
+ this.resolveAnimationWait();
140
+ }
104
141
 
105
142
  /**
106
143
  * Trigger a flash animation on this sprite
@@ -148,13 +185,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
148
185
  * });
149
186
  * ```
150
187
  */
151
- flash(options?: {
152
- type?: 'alpha' | 'tint' | 'both';
153
- duration?: number;
154
- cycles?: number;
155
- alpha?: number;
156
- tint?: number | string;
157
- }): void {
188
+ flash(options?: FlashOptions): void {
158
189
  const flashOptions = {
159
190
  type: options?.type || 'alpha',
160
191
  duration: options?.duration ?? 300,
@@ -199,12 +230,14 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
199
230
  * ```
200
231
  */
201
232
  resetAnimationState() {
233
+ if (this.animationRestoreState) {
234
+ this.finishTemporaryAnimation();
235
+ return;
236
+ }
202
237
  this.animationIsPlaying.set(false);
203
238
  this.animationCurrentIndex.set(0);
204
- if (this.animationSubscription) {
205
- this.animationSubscription.unsubscribe();
206
- this.animationSubscription = undefined;
207
- }
239
+ this.clearAnimationControls();
240
+ this.resolveAnimationWait();
208
241
  }
209
242
 
210
243
  /**
@@ -216,17 +249,19 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
216
249
  *
217
250
  * @param animationName - Name of the animation to play
218
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
219
254
  *
220
255
  * @example
221
256
  * ```ts
222
257
  * // Play attack animation 3 times
223
- * player.setAnimation('attack', 3);
258
+ * await player.setAnimation('attack', 3);
224
259
  *
225
260
  * // Play continuous spell animation
226
261
  * player.setAnimation('spell');
227
262
  * ```
228
263
  */
229
- setAnimation(animationName: string, nbTimes?: number): void;
264
+ setAnimation(animationName: string, nbTimes?: number, options?: AnimationRestoreOptions): Promise<void>;
230
265
  /**
231
266
  * Set a custom animation with temporary graphic change
232
267
  *
@@ -237,37 +272,68 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
237
272
  * @param animationName - Name of the animation to play
238
273
  * @param graphic - The graphic(s) to temporarily use during the animation
239
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
240
277
  *
241
278
  * @example
242
279
  * ```ts
243
280
  * // Play attack animation with temporary graphic change
244
- * player.setAnimation('attack', 'hero_attack', 3);
281
+ * await player.setAnimation('attack', 'hero_attack', 3);
245
282
  * ```
246
283
  */
247
- setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;
248
- setAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes?: number): void {
249
- if (this.animationIsPlaying()) return;
250
- this.animationIsPlaying.set(true);
251
- const previousAnimationName = this.animationName();
252
- const previousGraphics = this.graphics();
253
- this.animationCurrentIndex.set(0);
254
-
284
+ setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number, options?: AnimationRestoreOptions): Promise<void>;
285
+ setAnimation(
286
+ animationName: string,
287
+ graphicOrNbTimes?: string | string[] | number,
288
+ nbTimesOrOptions?: number | AnimationRestoreOptions,
289
+ options?: AnimationRestoreOptions
290
+ ): Promise<void> {
255
291
  let graphic: string | string[] | undefined;
256
292
  let finalNbTimes: number = Infinity;
293
+ let restoreOptions: AnimationRestoreOptions | undefined = options;
257
294
 
258
295
  // Handle overloads
259
296
  if (typeof graphicOrNbTimes === 'number') {
260
297
  // setAnimation(animationName, nbTimes)
261
298
  finalNbTimes = graphicOrNbTimes;
299
+ restoreOptions = typeof nbTimesOrOptions === 'object' ? nbTimesOrOptions : options;
262
300
  } else if (graphicOrNbTimes !== undefined) {
263
301
  // setAnimation(animationName, graphic, nbTimes)
264
302
  graphic = graphicOrNbTimes;
265
- finalNbTimes = nbTimes ?? Infinity;
303
+ if (typeof nbTimesOrOptions === 'number') {
304
+ finalNbTimes = nbTimesOrOptions;
305
+ } else {
306
+ finalNbTimes = Infinity;
307
+ restoreOptions = nbTimesOrOptions ?? options;
308
+ }
266
309
  } else {
267
310
  // setAnimation(animationName) - nbTimes remains Infinity
268
311
  finalNbTimes = Infinity;
269
312
  }
270
313
 
314
+ if (this.animationIsPlaying()) {
315
+ this.finishTemporaryAnimation();
316
+ }
317
+
318
+ const waitPromise =
319
+ finalNbTimes === Infinity
320
+ ? Promise.resolve()
321
+ : new Promise<void>((resolve) => {
322
+ this.animationWaitResolve = resolve;
323
+ });
324
+
325
+ this.animationIsPlaying.set(true);
326
+ const previousAnimationName =
327
+ restoreOptions?.restoreAnimationName ?? this.animationName();
328
+ const previousGraphics = restoreOptions?.restoreGraphics
329
+ ? [...restoreOptions.restoreGraphics]
330
+ : [...this.graphics()];
331
+ this.animationRestoreState = {
332
+ animationName: previousAnimationName,
333
+ graphics: previousGraphics,
334
+ };
335
+ this.animationCurrentIndex.set(0);
336
+
271
337
  // Temporarily change graphic if provided
272
338
  if (graphic !== undefined) {
273
339
  if (Array.isArray(graphic)) {
@@ -277,28 +343,26 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
277
343
  }
278
344
  }
279
345
 
280
- // Clean up any existing subscription
281
- if (this.animationSubscription) {
282
- this.animationSubscription.unsubscribe();
283
- }
346
+ this.clearAnimationControls();
284
347
 
285
348
  this.animationSubscription =
286
349
  this.animationCurrentIndex.observable.subscribe((index) => {
287
350
  if (index >= finalNbTimes) {
288
- this.animationCurrentIndex.set(0);
289
- this.animationName.set(previousAnimationName);
290
- // Reset graphic to previous value if it was changed
291
- if (graphic !== undefined) {
292
- this.graphics.set(previousGraphics);
293
- }
294
- this.animationIsPlaying.set(false);
295
- if (this.animationSubscription) {
296
- this.animationSubscription.unsubscribe();
297
- this.animationSubscription = undefined;
298
- }
351
+ this.finishTemporaryAnimation();
299
352
  }
300
353
  });
354
+
355
+ if (finalNbTimes !== Infinity) {
356
+ this.animationResetTimeout = setTimeout(() => {
357
+ if (this.animationIsPlaying()) {
358
+ this.finishTemporaryAnimation();
359
+ }
360
+ }, restoreOptions?.timeoutMs ?? Math.max(1000, finalNbTimes * 1000));
361
+ }
362
+
301
363
  this.animationName.set(animationName);
364
+
365
+ return waitPromise;
302
366
  }
303
367
 
304
368
  /**
@@ -306,10 +370,25 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
306
370
  *
307
371
  * @param id - Identifier of the component animation to play.
308
372
  * @param params - Parameters forwarded to the animation effect.
373
+ * @returns A promise resolved when the animation component calls `onFinish`.
309
374
  */
310
- showComponentAnimation(id: string, params: any) {
375
+ showComponentAnimation(id: string, params: any): Promise<void> {
311
376
  const engine = inject(RpgClientEngine);
312
- 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
+ });
313
392
  }
314
393
 
315
394
  /**
@@ -0,0 +1,338 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+ import { Hooks } from "@rpgjs/common";
3
+ import { ProjectileManager } from "./ProjectileManager";
4
+
5
+ describe("ProjectileManager", () => {
6
+ afterEach(() => {
7
+ vi.useRealTimers();
8
+ });
9
+
10
+ test("renders registered projectile components from compact spawn data", () => {
11
+ const onSpawn = vi.fn();
12
+ const hooks = new Hooks([{ projectiles: { onSpawn } }], "client");
13
+ const manager = new ProjectileManager(hooks);
14
+ const component = () => null;
15
+
16
+ manager.register("fireball", component);
17
+ manager.spawnBatch([
18
+ {
19
+ id: "p1",
20
+ type: "fireball",
21
+ origin: { x: 10, y: 20 },
22
+ direction: { x: 1, y: 0 },
23
+ speed: 100,
24
+ range: 500,
25
+ ttl: 5,
26
+ spawnTick: 1,
27
+ },
28
+ ]);
29
+
30
+ const current = manager.current();
31
+ expect(current).toHaveLength(1);
32
+ expect(current[0].component).toBe(component);
33
+ expect(current[0].props.x).toBeGreaterThanOrEqual(10);
34
+ expect(current[0].props.angle).toBe(0);
35
+ expect(onSpawn).toHaveBeenCalledWith(expect.objectContaining({ id: "p1", type: "fireball" }));
36
+ });
37
+
38
+ test("starts visuals at the spawn origin even when a server tick estimate exists", () => {
39
+ vi.useFakeTimers();
40
+ vi.setSystemTime(2000);
41
+
42
+ const hooks = new Hooks([], "client");
43
+ const manager = new ProjectileManager(hooks);
44
+ manager.register("arrow", () => null);
45
+ manager.spawnBatch([
46
+ {
47
+ id: "p-latency",
48
+ type: "arrow",
49
+ origin: { x: 0, y: 0 },
50
+ direction: { x: 1, y: 0 },
51
+ speed: 120,
52
+ range: 500,
53
+ ttl: 5,
54
+ spawnTick: 10,
55
+ },
56
+ ], {
57
+ currentServerTick: 16,
58
+ tickDurationMs: 1000 / 60,
59
+ });
60
+
61
+ const current = manager.current();
62
+ expect(current).toHaveLength(1);
63
+ expect(current[0].props.elapsed).toBeCloseTo(0, 3);
64
+ expect(current[0].props.x).toBeCloseTo(0, 3);
65
+ });
66
+
67
+ test("keeps delayed projectiles until their visual delay has elapsed", () => {
68
+ vi.useFakeTimers();
69
+ vi.setSystemTime(1000);
70
+
71
+ const hooks = new Hooks([], "client");
72
+ const manager = new ProjectileManager(hooks);
73
+ manager.register("spark", () => null);
74
+ manager.spawnBatch([
75
+ {
76
+ id: "p-delayed",
77
+ type: "spark",
78
+ origin: { x: 0, y: 0 },
79
+ direction: { x: 1, y: 0 },
80
+ speed: 100,
81
+ range: 500,
82
+ ttl: 5,
83
+ spawnTick: 1,
84
+ delay: 0.1,
85
+ },
86
+ ]);
87
+
88
+ vi.setSystemTime(1050);
89
+ manager.step();
90
+ expect(manager.current()).toHaveLength(0);
91
+
92
+ vi.setSystemTime(1110);
93
+ manager.step();
94
+ const current = manager.current();
95
+ expect(current).toHaveLength(1);
96
+ expect(current[0].props.elapsed).toBeCloseTo(0.01, 3);
97
+ });
98
+
99
+ test("keeps impacted projectiles briefly so components can react", () => {
100
+ const hooks = new Hooks([], "client");
101
+ const manager = new ProjectileManager(hooks);
102
+ manager.register("arrow", () => null);
103
+ manager.spawnBatch([
104
+ {
105
+ id: "p2",
106
+ type: "arrow",
107
+ origin: { x: 0, y: 0 },
108
+ direction: { x: 1, y: 0 },
109
+ speed: 100,
110
+ range: 500,
111
+ ttl: 5,
112
+ spawnTick: 1,
113
+ },
114
+ ]);
115
+
116
+ manager.impactBatch([{ id: "p2", x: 42, y: 0, distance: 42 }]);
117
+
118
+ const current = manager.current();
119
+ expect(current).toHaveLength(1);
120
+ expect(current[0].props.impact?.x).toBe(42);
121
+ expect(current[0].props.destroyed).toBe(true);
122
+ });
123
+
124
+ test("keeps hit destroys briefly even if the destroy packet arrives before impact", () => {
125
+ const hooks = new Hooks([], "client");
126
+ const manager = new ProjectileManager(hooks);
127
+ manager.register("arrow", () => null);
128
+ manager.spawnBatch([
129
+ {
130
+ id: "p3",
131
+ type: "arrow",
132
+ origin: { x: 0, y: 0 },
133
+ direction: { x: 1, y: 0 },
134
+ speed: 100,
135
+ range: 500,
136
+ ttl: 5,
137
+ spawnTick: 1,
138
+ },
139
+ ]);
140
+
141
+ manager.destroyBatch([{ id: "p3", reason: "hit", x: 48, y: 0, distance: 48 }]);
142
+
143
+ const current = manager.current();
144
+ expect(current).toHaveLength(1);
145
+ expect(current[0].props.impact?.x).toBe(48);
146
+ expect(current[0].props.destroyed).toBe(true);
147
+ });
148
+
149
+ test("freezes hit destroys at the authoritative impact position until the impact completes", () => {
150
+ vi.useFakeTimers();
151
+ vi.setSystemTime(1000);
152
+
153
+ const hooks = new Hooks([], "client");
154
+ const manager = new ProjectileManager(hooks);
155
+ manager.register("arrow", () => null);
156
+ manager.spawnBatch([
157
+ {
158
+ id: "p4",
159
+ type: "arrow",
160
+ origin: { x: 0, y: 0 },
161
+ direction: { x: 1, y: 0 },
162
+ speed: 100,
163
+ range: 500,
164
+ ttl: 5,
165
+ spawnTick: 1,
166
+ },
167
+ ]);
168
+
169
+ vi.setSystemTime(1200);
170
+ manager.destroyBatch([{ id: "p4", reason: "hit", x: 48, y: 0, distance: 48 }]);
171
+
172
+ vi.setSystemTime(1300);
173
+ manager.step();
174
+ let current = manager.current();
175
+ expect(current).toHaveLength(1);
176
+ expect(current[0].props.x).toBe(48);
177
+ expect(current[0].props.distance).toBe(48);
178
+ expect(current[0].props.impactProgress).toBeCloseTo(100 / 350, 3);
179
+
180
+ vi.setSystemTime(1600);
181
+ manager.step();
182
+ current = manager.current();
183
+ expect(current).toHaveLength(0);
184
+ });
185
+
186
+ test("clamps visual movement at the predicted impact without starting the impact animation", () => {
187
+ vi.useFakeTimers();
188
+ vi.setSystemTime(1000);
189
+
190
+ const hooks = new Hooks([], "client");
191
+ const manager = new ProjectileManager(hooks, () => ({
192
+ id: "p5",
193
+ targetId: "target",
194
+ x: 30,
195
+ y: 0,
196
+ distance: 30,
197
+ }));
198
+ manager.register("arrow", () => null);
199
+ manager.spawnBatch([
200
+ {
201
+ id: "p5",
202
+ type: "arrow",
203
+ origin: { x: 0, y: 0 },
204
+ direction: { x: 1, y: 0 },
205
+ speed: 100,
206
+ range: 500,
207
+ ttl: 5,
208
+ spawnTick: 1,
209
+ },
210
+ ]);
211
+
212
+ vi.setSystemTime(1200);
213
+ manager.step();
214
+ let current = manager.current();
215
+ expect(current).toHaveLength(1);
216
+ expect(current[0].props.x).toBe(20);
217
+ expect(current[0].props.impact).toBeUndefined();
218
+ expect(current[0].props.destroyed).toBe(false);
219
+
220
+ vi.setSystemTime(1400);
221
+ manager.step();
222
+ current = manager.current();
223
+ expect(current).toHaveLength(1);
224
+ expect(current[0].props.x).toBe(30);
225
+ expect(current[0].props.distance).toBe(30);
226
+ expect(current[0].props.impact).toBeUndefined();
227
+ expect(current[0].props.destroyed).toBe(false);
228
+
229
+ manager.impactBatch([{ id: "p5", targetId: "target", x: 32, y: 0, distance: 32 }]);
230
+ current = manager.current();
231
+ expect(current[0].props.x).toBe(30);
232
+ expect(current[0].props.distance).toBe(30);
233
+ expect(current[0].props.impact?.x).toBe(32);
234
+ expect(current[0].props.destroyed).toBe(true);
235
+ });
236
+
237
+ test("uses the authoritative impact position when the predicted target differs", () => {
238
+ vi.useFakeTimers();
239
+ vi.setSystemTime(1000);
240
+
241
+ const hooks = new Hooks([], "client");
242
+ const manager = new ProjectileManager(hooks, () => ({
243
+ id: "p7",
244
+ targetId: "wall",
245
+ x: 30,
246
+ y: 0,
247
+ distance: 30,
248
+ }));
249
+ manager.register("arrow", () => null);
250
+ manager.spawnBatch([
251
+ {
252
+ id: "p7",
253
+ type: "arrow",
254
+ origin: { x: 0, y: 0 },
255
+ direction: { x: 1, y: 0 },
256
+ speed: 100,
257
+ range: 500,
258
+ ttl: 5,
259
+ spawnTick: 1,
260
+ },
261
+ ]);
262
+
263
+ vi.setSystemTime(1400);
264
+ manager.step();
265
+ manager.impactBatch([{ id: "p7", targetId: "target", x: 45, y: 0, distance: 45 }]);
266
+
267
+ const current = manager.current();
268
+ expect(current[0].props.x).toBe(45);
269
+ expect(current[0].props.distance).toBe(45);
270
+ expect(current[0].props.impact?.x).toBe(45);
271
+ });
272
+
273
+ test("keeps an unconfirmed predicted impact clamped until the server resolves it", () => {
274
+ vi.useFakeTimers();
275
+ vi.setSystemTime(1000);
276
+
277
+ const hooks = new Hooks([], "client");
278
+ const manager = new ProjectileManager(hooks, () => ({
279
+ id: "p6",
280
+ targetId: "ignored",
281
+ x: 30,
282
+ y: 0,
283
+ distance: 30,
284
+ }));
285
+ manager.register("arrow", () => null);
286
+ manager.spawnBatch([
287
+ {
288
+ id: "p6",
289
+ type: "arrow",
290
+ origin: { x: 0, y: 0 },
291
+ direction: { x: 1, y: 0 },
292
+ speed: 100,
293
+ range: 500,
294
+ ttl: 5,
295
+ spawnTick: 1,
296
+ },
297
+ ]);
298
+
299
+ vi.setSystemTime(1400);
300
+ manager.step();
301
+ expect(manager.current()[0].props.x).toBe(30);
302
+
303
+ vi.setSystemTime(1900);
304
+ manager.step();
305
+ const current = manager.current();
306
+ expect(current).toHaveLength(1);
307
+ expect(current[0].props.x).toBe(30);
308
+ expect(current[0].props.impact).toBeUndefined();
309
+ expect(current[0].props.destroyed).toBe(false);
310
+
311
+ manager.destroyBatch([{ id: "p6", reason: "range" }]);
312
+ expect(manager.current()[0].props.x).toBe(30);
313
+ expect(manager.current()[0].props.destroyed).toBe(true);
314
+ });
315
+
316
+ test("skips local impact prediction when the server marks the projectile as non-predictable", () => {
317
+ const hooks = new Hooks([], "client");
318
+ const predictionResolver = vi.fn();
319
+ const manager = new ProjectileManager(hooks, predictionResolver);
320
+ manager.register("arrow", () => null);
321
+
322
+ manager.spawnBatch([
323
+ {
324
+ id: "p-no-predict",
325
+ type: "arrow",
326
+ origin: { x: 0, y: 0 },
327
+ direction: { x: 1, y: 0 },
328
+ speed: 100,
329
+ range: 500,
330
+ ttl: 5,
331
+ spawnTick: 1,
332
+ predictImpact: false,
333
+ },
334
+ ]);
335
+
336
+ expect(predictionResolver).not.toHaveBeenCalled();
337
+ });
338
+ });