@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,11 +1,12 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
+ import BuiltinSceneMap from "./components/scenes/draw-map.ce";
2
3
  import { inject } from './core/inject'
3
- import { signal, bootstrapCanvas, Howl, trigger } from "canvasengine";
4
+ import { signal, bootstrapCanvas, Howl, trigger, type Trigger } from "canvasengine";
4
5
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
5
6
  import { LoadMapService, LoadMapToken } from "./services/loadMap";
6
7
  import { RpgSound } from "./Sound";
7
8
  import { RpgResource } from "./Resource";
8
- import { Hooks, ModulesToken, Direction } from "@rpgjs/common";
9
+ import { Hooks, ModulesToken, Direction, normalizeLightingState, Vector2 } from "@rpgjs/common";
9
10
  import { load } from "@signe/sync";
10
11
  import { RpgClientMap } from "./Game/Map"
11
12
  import { RpgGui } from "./Gui/Gui";
@@ -14,13 +15,23 @@ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, swit
14
15
  import { GlobalConfigToken } from "./module";
15
16
  import * as PIXI from "pixi.js";
16
17
  import { PrebuiltComponentAnimations } from "./components/animations";
18
+ import TextComponent from "./components/dynamics/text.ce";
19
+ import BarComponent from "./components/dynamics/bar.ce";
20
+ import ShapeComponent from "./components/dynamics/shape.ce";
21
+ import ImageComponent from "./components/dynamics/image.ce";
17
22
  import {
18
23
  PredictionController,
19
24
  type PredictionHistoryEntry,
20
25
  type PredictionState,
26
+ type RpgActionInput,
27
+ type RpgActionName,
21
28
  } from "@rpgjs/common";
22
29
  import { NotificationManager } from "./Gui/NotificationManager";
23
30
  import { SaveClientService } from "./services/save";
31
+ import { getCanMoveValue } from "./utils/readPropValue";
32
+ import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
33
+ import { normalizeActionInput } from "./services/actionInput";
34
+ import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
24
35
 
25
36
  interface MovementTrajectoryPoint {
26
37
  frame: number;
@@ -32,6 +43,17 @@ interface MovementTrajectoryPoint {
32
43
  direction?: Direction;
33
44
  }
34
45
 
46
+ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
47
+ start(config?: T): Promise<void>;
48
+ };
49
+
50
+ type MapShakeOptions = {
51
+ intensity?: number;
52
+ duration?: number;
53
+ frequency?: number;
54
+ direction?: string;
55
+ };
56
+
35
57
  export class RpgClientEngine<T = any> {
36
58
  private guiService: RpgGui;
37
59
  private webSocket: AbstractWebsocket;
@@ -41,13 +63,16 @@ export class RpgClientEngine<T = any> {
41
63
  private selector: HTMLElement;
42
64
  public globalConfig: T;
43
65
  public sceneComponent: any;
66
+ public sceneMapComponent: any = BuiltinSceneMap;
44
67
  stopProcessingInput = false;
45
68
  width = signal("100%");
46
69
  height = signal("100%");
47
- spritesheets: Map<string, any> = new Map();
70
+ spritesheets: Map<string | number, any> = new Map();
48
71
  sounds: Map<string, any> = new Map();
49
72
  componentAnimations: any[] = [];
50
- private spritesheetResolver?: (id: string) => any | Promise<any>;
73
+ projectiles: ProjectileManager;
74
+ pointer: ClientPointerContext = createClientPointerContext();
75
+ private spritesheetResolver?: (id: string | number) => any | Promise<any>;
51
76
  private soundResolver?: (id: string) => any | Promise<any>;
52
77
  particleSettings: {
53
78
  emitters: any[]
@@ -61,10 +86,11 @@ export class RpgClientEngine<T = any> {
61
86
  playerIdSignal = signal<string | null>(null);
62
87
  spriteComponentsBehind = signal<any[]>([]);
63
88
  spriteComponentsInFront = signal<any[]>([]);
89
+ spriteComponents: Map<string, any> = new Map();
64
90
  /** ID of the sprite that the camera should follow. null means follow the current player */
65
91
  cameraFollowTargetId = signal<string | null>(null);
66
92
  /** Trigger for map shake animation */
67
- mapShakeTrigger = trigger();
93
+ mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
68
94
 
69
95
  controlsReady = signal(undefined);
70
96
  gamePause = signal(false);
@@ -76,6 +102,8 @@ export class RpgClientEngine<T = any> {
76
102
  private pendingPredictionFrames: number[] = [];
77
103
  private lastClientPhysicsStepAt = 0;
78
104
  private frameOffset = 0;
105
+ private latestServerTick?: number;
106
+ private latestServerTickAt = 0;
79
107
  // Ping/Pong for RTT measurement
80
108
  private rtt: number = 0; // Round-trip time in ms
81
109
  private pingInterval: any = null;
@@ -96,6 +124,9 @@ export class RpgClientEngine<T = any> {
96
124
  // Store subscriptions and event listeners for cleanup
97
125
  private tickSubscriptions: any[] = [];
98
126
  private resizeHandler?: () => void;
127
+ private pointerMoveHandler?: (event: PointerEvent) => void;
128
+ private pointerCanvas?: HTMLCanvasElement;
129
+ private pendingSyncPackets: any[] = [];
99
130
  private notificationManager: NotificationManager = new NotificationManager();
100
131
 
101
132
  constructor(public context) {
@@ -103,6 +134,10 @@ export class RpgClientEngine<T = any> {
103
134
  this.guiService = inject(RpgGui);
104
135
  this.loadMapService = inject(LoadMapToken);
105
136
  this.hooks = inject<Hooks>(ModulesToken);
137
+ this.projectiles = new ProjectileManager(
138
+ this.hooks,
139
+ (projectile) => this.predictProjectileImpact(projectile),
140
+ );
106
141
  this.globalConfig = inject(GlobalConfigToken)
107
142
 
108
143
  if (!this.globalConfig) {
@@ -123,6 +158,13 @@ export class RpgClientEngine<T = any> {
123
158
  component: PrebuiltComponentAnimations.Animation
124
159
  })
125
160
 
161
+ this.registerSpriteComponent("rpg:text", TextComponent);
162
+ this.registerSpriteComponent("rpg:hpBar", BarComponent);
163
+ this.registerSpriteComponent("rpg:spBar", BarComponent);
164
+ this.registerSpriteComponent("rpg:bar", BarComponent);
165
+ this.registerSpriteComponent("rpg:shape", ShapeComponent);
166
+ this.registerSpriteComponent("rpg:image", ImageComponent);
167
+
126
168
  this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
127
169
  this.initializePredictionController();
128
170
  }
@@ -173,6 +215,22 @@ export class RpgClientEngine<T = any> {
173
215
  this.sceneMap = new RpgClientMap()
174
216
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
175
217
  this.sceneMap.loadPhysic();
218
+ this.resolveSceneMapComponent();
219
+
220
+ const saveClient = inject(SaveClientService);
221
+ saveClient.initialize();
222
+ this.initListeners();
223
+ this.guiService._initialize();
224
+
225
+ try {
226
+ await this.webSocket.connection();
227
+ }
228
+ catch (error) {
229
+ this.stopPingPong();
230
+ await this.callConnectError(error);
231
+ throw error;
232
+ }
233
+
176
234
  this.selector = document.body.querySelector("#rpg") as HTMLElement;
177
235
 
178
236
  const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
@@ -183,8 +241,10 @@ export class RpgClientEngine<T = any> {
183
241
  );
184
242
  this.canvasApp = app;
185
243
  this.canvasElement = canvasElement;
186
- this.renderer = app.renderer as PIXI.Renderer;
244
+ this.renderer = app.renderer as unknown as PIXI.Renderer;
245
+ this.setupPointerTracking();
187
246
  this.tick = canvasElement?.propObservables?.context['tick'].observable
247
+ this.flushPendingSyncPackets();
188
248
 
189
249
  const inputCheckSubscription = this.tick.subscribe(() => {
190
250
  if (Date.now() - this.lastInputTime > 100) {
@@ -200,12 +260,13 @@ export class RpgClientEngine<T = any> {
200
260
  this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
201
261
  this.hooks.callHooks("client-sounds-load", this).subscribe();
202
262
  this.hooks.callHooks("client-soundResolver-load", this).subscribe();
203
-
263
+
204
264
  RpgSound.init(this);
205
265
  RpgResource.init(this);
206
266
  this.hooks.callHooks("client-gui-load", this).subscribe();
207
267
  this.hooks.callHooks("client-particles-load", this).subscribe();
208
268
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
269
+ this.hooks.callHooks("client-projectiles-load", this).subscribe();
209
270
  this.hooks.callHooks("client-sprite-load", this).subscribe();
210
271
 
211
272
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
@@ -218,6 +279,7 @@ export class RpgClientEngine<T = any> {
218
279
 
219
280
  const tickSubscription = this.tick.subscribe((tick) => {
220
281
  this.stepClientPhysicsTick();
282
+ this.projectiles.step();
221
283
  this.flushPendingPredictedStates();
222
284
  this.flushPendingMovePath();
223
285
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
@@ -231,13 +293,56 @@ export class RpgClientEngine<T = any> {
231
293
  });
232
294
  this.tickSubscriptions.push(tickSubscription);
233
295
 
234
- await this.webSocket.connection(() => {
235
- const saveClient = inject(SaveClientService);
236
- saveClient.initialize(this.webSocket);
237
- this.initListeners()
238
- this.guiService._initialize()
239
- this.startPingPong();
240
- });
296
+ this.startPingPong();
297
+ }
298
+
299
+ private resolveSceneMapComponent() {
300
+ const components = this.hooks.getHookFunctions("client-sceneMap-component");
301
+ const component = components[components.length - 1];
302
+ if (component) {
303
+ this.sceneMapComponent = component;
304
+ }
305
+ }
306
+
307
+ private setupPointerTracking() {
308
+ const renderer = this.renderer as any;
309
+ const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
310
+
311
+ if (!canvas || typeof canvas.addEventListener !== "function") {
312
+ return;
313
+ }
314
+
315
+ this.pointerCanvas = canvas;
316
+ this.pointerMoveHandler = (event: PointerEvent) => {
317
+ const rect = canvas.getBoundingClientRect();
318
+ const screen = {
319
+ x: event.clientX - rect.left,
320
+ y: event.clientY - rect.top,
321
+ };
322
+ const viewport = this.findViewportInstance();
323
+ let world = screen;
324
+
325
+ if (viewport && typeof viewport.toWorld === "function") {
326
+ const point = viewport.toWorld(screen.x, screen.y);
327
+ world = { x: Number(point.x), y: Number(point.y) };
328
+ } else if (viewport && typeof viewport.toLocal === "function") {
329
+ const point = viewport.toLocal(screen);
330
+ world = { x: Number(point.x), y: Number(point.y) };
331
+ }
332
+
333
+ this.pointer.update(screen, world);
334
+ };
335
+
336
+ canvas.addEventListener("pointermove", this.pointerMoveHandler);
337
+ canvas.addEventListener("pointerdown", this.pointerMoveHandler);
338
+ }
339
+
340
+ private findViewportInstance(): any {
341
+ const children = (this.canvasApp as any)?.stage?.children ?? [];
342
+ return children.find((child: any) => (
343
+ typeof child?.toWorld === "function"
344
+ || child?.constructor?.name === "Viewport"
345
+ ));
241
346
  }
242
347
 
243
348
  private prepareSyncPayload(data: any): any {
@@ -288,51 +393,11 @@ export class RpgClientEngine<T = any> {
288
393
 
289
394
  private initListeners() {
290
395
  this.webSocket.on("sync", (data) => {
291
- if (data.pId) {
292
- this.playerIdSignal.set(data.pId);
293
- // Signal that player ID was received
294
- this.playerIdReceived$.next(true);
295
- }
296
-
297
- if (this.sceneResetQueued) {
298
- this.sceneMap.reset();
299
- this.sceneMap.loadPhysic();
300
- this.sceneResetQueued = false;
301
- }
302
-
303
- // Apply client-side prediction filtering and server reconciliation
304
- this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
305
-
306
- const ack = data?.ack;
307
- const normalizedAck =
308
- ack && typeof ack.frame === "number"
309
- ? this.normalizeAckWithSyncState(ack, data)
310
- : undefined;
311
- const payload = this.prepareSyncPayload(data);
312
- load(this.sceneMap, payload, true);
313
-
314
- if (normalizedAck) {
315
- this.applyServerAck(normalizedAck);
316
- }
317
-
318
- for (const playerId in payload.players ?? {}) {
319
- const player = payload.players[playerId]
320
- if (!player._param) continue
321
- for (const param in player._param) {
322
- this.sceneMap.players()[playerId]._param()[param] = player._param[param]
323
- }
324
- }
325
-
326
- // Check if players and events are present in sync data
327
- const players = payload.players || this.sceneMap.players();
328
- if (players && Object.keys(players).length > 0) {
329
- this.playersReceived$.next(true);
330
- }
331
-
332
- const events = payload.events || this.sceneMap.events();
333
- if (events !== undefined) {
334
- this.eventsReceived$.next(true);
396
+ if (!this.tick) {
397
+ this.pendingSyncPackets.push(data);
398
+ return;
335
399
  }
400
+ this.applySyncPacket(data);
336
401
  });
337
402
 
338
403
  // Handle pong responses for RTT measurement
@@ -344,6 +409,7 @@ export class RpgClientEngine<T = any> {
344
409
  // This helps us estimate which server tick corresponds to each client input frame
345
410
  const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
346
411
  const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
412
+ this.updateServerTickEstimate(estimatedServerTickNow, now);
347
413
 
348
414
  // Update frame offset (only if we have inputs to calibrate with)
349
415
  if (this.inputFrameCounter > 0) {
@@ -355,6 +421,10 @@ export class RpgClientEngine<T = any> {
355
421
 
356
422
  this.webSocket.on("changeMap", (data) => {
357
423
  this.sceneResetQueued = true;
424
+ this.sceneMap.weatherState.set(null);
425
+ this.sceneMap.lightingState.set(null);
426
+ this.sceneMap.clearLightSpots();
427
+ this.projectiles.clear();
358
428
  // Reset camera follow to default (follow current player) when changing maps
359
429
  this.cameraFollowTargetId.set(null);
360
430
  const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
@@ -370,19 +440,50 @@ export class RpgClientEngine<T = any> {
370
440
  this.getComponentAnimation(id).displayEffect(params, player || position)
371
441
  });
372
442
 
443
+ this.webSocket.on("projectile:spawnBatch", (data) => {
444
+ this.projectiles.spawnBatch(data?.projectiles ?? [], {
445
+ currentServerTick: this.estimateServerTick(),
446
+ tickDurationMs: this.getPhysicsTickDurationMs(),
447
+ });
448
+ });
449
+
450
+ this.webSocket.on("projectile:impactBatch", (data) => {
451
+ this.projectiles.impactBatch(data?.impacts ?? []);
452
+ });
453
+
454
+ this.webSocket.on("projectile:destroyBatch", (data) => {
455
+ this.projectiles.destroyBatch(data?.projectiles ?? []);
456
+ });
457
+
458
+ this.webSocket.on("projectile:clear", () => {
459
+ this.projectiles.clear();
460
+ });
461
+
373
462
  this.webSocket.on("notification", (data) => {
374
463
  this.notificationManager.add(data);
375
464
  });
376
465
 
377
- this.webSocket.on("setAnimation", (data) => {
378
- const { animationName, nbTimes, object, graphic } = data;
379
- const player = this.sceneMap.getObjectById(object);
380
- if (graphic !== undefined) {
381
- player.setAnimation(animationName, graphic, nbTimes);
382
- } else {
383
- player.setAnimation(animationName, nbTimes);
384
- }
385
- })
466
+ this.webSocket.on("setAnimation", (data) => {
467
+ const {
468
+ animationName,
469
+ nbTimes,
470
+ object,
471
+ graphic,
472
+ restoreAnimationName,
473
+ restoreGraphics,
474
+ } = data;
475
+ const player = object ? this.sceneMap.getObjectById(object) : undefined;
476
+ if (!player) return;
477
+ const restoreOptions = {
478
+ restoreAnimationName,
479
+ restoreGraphics,
480
+ };
481
+ if (graphic !== undefined) {
482
+ player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
483
+ } else {
484
+ player.setAnimation(animationName, nbTimes, restoreOptions);
485
+ }
486
+ })
386
487
 
387
488
  this.webSocket.on("playSound", (data) => {
388
489
  const { soundId, volume, loop } = data;
@@ -413,7 +514,7 @@ export class RpgClientEngine<T = any> {
413
514
 
414
515
  this.webSocket.on("shakeMap", (data) => {
415
516
  const { intensity, duration, frequency, direction } = data || {};
416
- (this.mapShakeTrigger as any).start({
517
+ this.mapShakeTrigger.start({
417
518
  intensity,
418
519
  duration,
419
520
  frequency,
@@ -447,6 +548,14 @@ export class RpgClientEngine<T = any> {
447
548
  });
448
549
  });
449
550
 
551
+ this.webSocket.on("lightingState", (data) => {
552
+ const raw = (data && typeof data === "object" && "value" in data)
553
+ ? (data as any).value
554
+ : data;
555
+
556
+ this.sceneMap.lightingState.set(normalizeLightingState(raw));
557
+ });
558
+
450
559
  this.webSocket.on('open', () => {
451
560
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
452
561
  // Start ping/pong for synchronization
@@ -460,10 +569,72 @@ export class RpgClientEngine<T = any> {
460
569
  })
461
570
 
462
571
  this.webSocket.on('error', (error) => {
463
- this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
572
+ void this.callConnectError(error);
464
573
  })
465
574
  }
466
575
 
576
+ private async callConnectError(error: any) {
577
+ await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
578
+ }
579
+
580
+ private flushPendingSyncPackets() {
581
+ const packets = this.pendingSyncPackets;
582
+ this.pendingSyncPackets = [];
583
+ packets.forEach((packet) => this.applySyncPacket(packet));
584
+ }
585
+
586
+ private applySyncPacket(data: any) {
587
+ if (data.pId) {
588
+ this.playerIdSignal.set(data.pId);
589
+ // Signal that player ID was received
590
+ this.playerIdReceived$.next(true);
591
+ }
592
+
593
+ if (this.sceneResetQueued) {
594
+ const weatherState = this.sceneMap.weatherState();
595
+ const lightingState = this.sceneMap.lightingState();
596
+ this.sceneMap.reset();
597
+ this.sceneMap.weatherState.set(weatherState);
598
+ this.sceneMap.lightingState.set(lightingState);
599
+ this.sceneMap.loadPhysic();
600
+ this.sceneResetQueued = false;
601
+ }
602
+
603
+ // Apply client-side prediction filtering and server reconciliation
604
+ this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
605
+
606
+ const ack = data?.ack;
607
+ const normalizedAck =
608
+ ack && typeof ack.frame === "number"
609
+ ? this.normalizeAckWithSyncState(ack, data)
610
+ : undefined;
611
+ const payload = this.prepareSyncPayload(data);
612
+ load(this.sceneMap, payload, true);
613
+
614
+ if (normalizedAck) {
615
+ this.applyServerAck(normalizedAck);
616
+ }
617
+
618
+ for (const playerId in payload.players ?? {}) {
619
+ const player = payload.players[playerId]
620
+ if (!player._param) continue
621
+ for (const param in player._param) {
622
+ this.sceneMap.players()[playerId]._param()[param] = player._param[param]
623
+ }
624
+ }
625
+
626
+ // Check if players and events are present in sync data
627
+ const players = payload.players || this.sceneMap.players();
628
+ if (players && Object.keys(players).length > 0) {
629
+ this.playersReceived$.next(true);
630
+ }
631
+
632
+ const events = payload.events || this.sceneMap.events();
633
+ if (events !== undefined) {
634
+ this.eventsReceived$.next(true);
635
+ }
636
+ }
637
+
467
638
  /**
468
639
  * Start periodic ping/pong for client-server synchronization
469
640
  *
@@ -560,12 +731,19 @@ export class RpgClientEngine<T = any> {
560
731
  room: mapId,
561
732
  query: transferToken ? { transferToken } : undefined,
562
733
  })
563
- await this.webSocket.reconnect(() => {
564
- const saveClient = inject(SaveClientService);
565
- saveClient.initialize(this.webSocket);
566
- this.initListeners()
567
- this.guiService._initialize()
568
- })
734
+ try {
735
+ await this.webSocket.reconnect(() => {
736
+ const saveClient = inject(SaveClientService);
737
+ saveClient.initialize();
738
+ this.initListeners()
739
+ this.guiService._initialize()
740
+ })
741
+ }
742
+ catch (error) {
743
+ this.stopPingPong();
744
+ await this.callConnectError(error);
745
+ throw error;
746
+ }
569
747
  const res = await this.loadMapService.load(mapId)
570
748
  this.sceneMap.data.set(res)
571
749
 
@@ -623,7 +801,7 @@ export class RpgClientEngine<T = any> {
623
801
  * });
624
802
  * ```
625
803
  */
626
- setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
804
+ setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
627
805
  this.spritesheetResolver = resolver;
628
806
  }
629
807
 
@@ -634,7 +812,7 @@ export class RpgClientEngine<T = any> {
634
812
  * If not found and a resolver is set, it calls the resolver to create the spritesheet.
635
813
  * The resolved spritesheet is automatically cached for future use.
636
814
  *
637
- * @param id - The spritesheet ID to retrieve
815
+ * @param id - The spritesheet ID or legacy tile ID to retrieve
638
816
  * @returns The spritesheet if found or created, or undefined if not found and no resolver
639
817
  * @returns Promise<any> if the resolver is asynchronous
640
818
  *
@@ -647,7 +825,7 @@ export class RpgClientEngine<T = any> {
647
825
  * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
648
826
  * ```
649
827
  */
650
- getSpriteSheet(id: string): any | Promise<any> {
828
+ getSpriteSheet(id: string | number): any | Promise<any> {
651
829
  // Check cache first
652
830
  if (this.spritesheets.has(id)) {
653
831
  return this.spritesheets.get(id);
@@ -1081,6 +1259,45 @@ export class RpgClientEngine<T = any> {
1081
1259
  return component
1082
1260
  }
1083
1261
 
1262
+ /**
1263
+ * Register a reusable sprite component that can be addressed by the server.
1264
+ *
1265
+ * Server-side component definitions only carry the component id and
1266
+ * serializable props. The client registry maps that id to the CanvasEngine
1267
+ * component that performs the actual rendering.
1268
+ *
1269
+ * @param id - Stable component id used by server component definitions
1270
+ * @param component - CanvasEngine component to render for this id
1271
+ * @returns The registered component
1272
+ *
1273
+ * @example
1274
+ * ```ts
1275
+ * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
1276
+ * ```
1277
+ */
1278
+ registerSpriteComponent(id: string, component: any) {
1279
+ this.spriteComponents.set(id, component);
1280
+ return component;
1281
+ }
1282
+
1283
+ /**
1284
+ * Get a reusable sprite component by id.
1285
+ *
1286
+ * @param id - Component id registered on the client
1287
+ * @returns The CanvasEngine component, or undefined when missing
1288
+ */
1289
+ getSpriteComponent(id: string) {
1290
+ return this.spriteComponents.get(id);
1291
+ }
1292
+
1293
+ registerProjectileComponent(type: string, component: any) {
1294
+ return this.projectiles.register(type, component);
1295
+ }
1296
+
1297
+ getProjectileComponent(type: string) {
1298
+ return this.projectiles.get(type);
1299
+ }
1300
+
1084
1301
  /**
1085
1302
  * Add a component animation to the engine
1086
1303
  *
@@ -1164,16 +1381,43 @@ export class RpgClientEngine<T = any> {
1164
1381
  * duration: 1000,
1165
1382
  * onFinish: () => console.log('Fade complete')
1166
1383
  * });
1384
+ *
1385
+ * // Wait until the transition component calls onFinish
1386
+ * await engine.startTransition('fade', { duration: 1000 });
1167
1387
  * ```
1168
1388
  */
1169
- startTransition(id: string, props: any = {}) {
1389
+ startTransition(id: string, props: any = {}): Promise<void> {
1170
1390
  if (!this.guiService.exists(id)) {
1171
1391
  throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
1172
1392
  }
1173
- this.guiService.display(id, props);
1393
+ return new Promise<void>((resolve) => {
1394
+ let finished = false;
1395
+ const finish = (data?: any) => {
1396
+ if (finished) return;
1397
+ finished = true;
1398
+ props?.onFinish?.(data);
1399
+ resolve();
1400
+ };
1401
+
1402
+ this.guiService.display(id, {
1403
+ ...props,
1404
+ onFinish: finish,
1405
+ });
1406
+ });
1174
1407
  }
1175
1408
 
1176
1409
  async processInput({ input }: { input: Direction }) {
1410
+ if (this.stopProcessingInput) return;
1411
+
1412
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1413
+ const canMove =
1414
+ !currentPlayer ||
1415
+ getCanMoveValue(currentPlayer);
1416
+ if (!canMove) {
1417
+ this.interruptCurrentPlayerMovement(currentPlayer);
1418
+ return;
1419
+ }
1420
+
1177
1421
  const timestamp = Date.now();
1178
1422
  let frame: number;
1179
1423
  let tick: number;
@@ -1188,7 +1432,6 @@ export class RpgClientEngine<T = any> {
1188
1432
  this.inputFrameCounter = frame;
1189
1433
  this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
1190
1434
 
1191
- const currentPlayer = this.sceneMap.getCurrentPlayer();
1192
1435
  const bodyReady = this.ensureCurrentPlayerBody();
1193
1436
  if (currentPlayer && bodyReady) {
1194
1437
  currentPlayer.changeDirection(input);
@@ -1205,10 +1448,25 @@ export class RpgClientEngine<T = any> {
1205
1448
  this.lastInputTime = Date.now();
1206
1449
  }
1207
1450
 
1208
- processAction({ action }: { action: number }) {
1451
+ processAction(action: RpgActionName, data?: any): void;
1452
+ processAction(action: RpgActionInput): void;
1453
+ processAction(action: RpgActionName | RpgActionInput, data?: any): void {
1209
1454
  if (this.stopProcessingInput) return;
1210
- this.hooks.callHooks("client-engine-onInput", this, { input: 'action', playerId: this.playerId }).subscribe();
1211
- this.webSocket.emit('action', { action })
1455
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1456
+ const canMove =
1457
+ !currentPlayer ||
1458
+ getCanMoveValue(currentPlayer);
1459
+ if (!canMove) return;
1460
+
1461
+ const payload = normalizeActionInput(action as any, data);
1462
+
1463
+ this.hooks.callHooks("client-engine-onInput", this, {
1464
+ input: payload.action,
1465
+ action: payload.action,
1466
+ data: payload.data,
1467
+ playerId: this.playerId,
1468
+ }).subscribe();
1469
+ this.webSocket.emit('action', payload)
1212
1470
  }
1213
1471
 
1214
1472
  get PIXI() {
@@ -1228,7 +1486,71 @@ export class RpgClientEngine<T = any> {
1228
1486
  }
1229
1487
 
1230
1488
  private getPhysicsTick(): number {
1231
- return this.sceneMap?.getTick?.() ?? 0;
1489
+ return (this.sceneMap as any)?.getTick?.() ?? 0;
1490
+ }
1491
+
1492
+ private getPhysicsTickDurationMs(): number {
1493
+ const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
1494
+ return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
1495
+ ? timeStep * 1000
1496
+ : 1000 / 60;
1497
+ }
1498
+
1499
+ private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
1500
+ if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
1501
+ return;
1502
+ }
1503
+ this.latestServerTick = serverTick;
1504
+ this.latestServerTickAt = now;
1505
+ }
1506
+
1507
+ private estimateServerTick(now = Date.now()): number | undefined {
1508
+ if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
1509
+ return undefined;
1510
+ }
1511
+ const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
1512
+ return this.latestServerTick + elapsedTicks;
1513
+ }
1514
+
1515
+ private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
1516
+ if (projectile.predictImpact === false) {
1517
+ return null;
1518
+ }
1519
+ const sceneMap = this.sceneMap as any;
1520
+ if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
1521
+ return null;
1522
+ }
1523
+ const origin = projectile.origin;
1524
+ const direction = projectile.direction;
1525
+ if (
1526
+ !origin ||
1527
+ !direction ||
1528
+ !Number.isFinite(origin.x) ||
1529
+ !Number.isFinite(origin.y) ||
1530
+ !Number.isFinite(direction.x) ||
1531
+ !Number.isFinite(direction.y) ||
1532
+ (direction.x === 0 && direction.y === 0)
1533
+ ) {
1534
+ return null;
1535
+ }
1536
+
1537
+ const hit = sceneMap.physic.raycast(
1538
+ new Vector2(origin.x, origin.y),
1539
+ new Vector2(direction.x, direction.y),
1540
+ projectile.range,
1541
+ projectile.collisionMask,
1542
+ (entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
1543
+ );
1544
+ if (!hit) {
1545
+ return null;
1546
+ }
1547
+ return {
1548
+ id: projectile.id,
1549
+ targetId: hit.entity.uuid,
1550
+ x: hit.point.x,
1551
+ y: hit.point.y,
1552
+ distance: hit.distance,
1553
+ };
1232
1554
  }
1233
1555
 
1234
1556
  private ensureCurrentPlayerBody(): boolean {
@@ -1337,6 +1659,14 @@ export class RpgClientEngine<T = any> {
1337
1659
  if (!this.predictionEnabled || !this.prediction) {
1338
1660
  return;
1339
1661
  }
1662
+ const player = this.sceneMap?.getCurrentPlayer?.() as any;
1663
+ if (
1664
+ player &&
1665
+ !getCanMoveValue(player)
1666
+ ) {
1667
+ this.interruptCurrentPlayerMovement(player);
1668
+ return;
1669
+ }
1340
1670
  const pendingInputs = this.prediction.getPendingInputs();
1341
1671
  if (pendingInputs.length === 0) {
1342
1672
  return;
@@ -1475,6 +1805,34 @@ export class RpgClientEngine<T = any> {
1475
1805
  this.lastMovePathSentFrame = 0;
1476
1806
  }
1477
1807
 
1808
+ /**
1809
+ * Stop local movement immediately and discard pending predicted movement.
1810
+ *
1811
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
1812
+ * startup, or any client-side state where already buffered movement inputs
1813
+ * must not be replayed after server reconciliation.
1814
+ *
1815
+ * @param player - Player object to stop. Defaults to the current player.
1816
+ * @returns `true` when a player was found and interrupted.
1817
+ *
1818
+ * @example
1819
+ * ```ts
1820
+ * engine.interruptCurrentPlayerMovement();
1821
+ * ```
1822
+ */
1823
+ interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
1824
+ if (!player) {
1825
+ return false;
1826
+ }
1827
+ (this.sceneMap as any)?.stopMovement?.(player);
1828
+ this.prediction?.clearPendingInputs();
1829
+ this.pendingPredictionFrames = [];
1830
+ this.lastInputTime = 0;
1831
+ this.lastMovePathSentAt = Date.now();
1832
+ this.lastMovePathSentFrame = this.inputFrameCounter;
1833
+ return true;
1834
+ }
1835
+
1478
1836
  /**
1479
1837
  * Trigger a flash animation on a sprite
1480
1838
  *
@@ -1542,6 +1900,7 @@ export class RpgClientEngine<T = any> {
1542
1900
  }
1543
1901
 
1544
1902
  private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
1903
+ this.updateServerTickEstimate(ack.serverTick);
1545
1904
  if (this.predictionEnabled && this.prediction) {
1546
1905
  const result = this.prediction.applyServerAck({
1547
1906
  frame: ack.frame,
@@ -1560,7 +1919,7 @@ export class RpgClientEngine<T = any> {
1560
1919
  if (typeof ack.x !== "number" || typeof ack.y !== "number") {
1561
1920
  return;
1562
1921
  }
1563
- const player = this.getCurrentPlayer();
1922
+ const player = this.getCurrentPlayer() as any;
1564
1923
  const myId = this.playerIdSignal();
1565
1924
  if (!player || !myId) {
1566
1925
  return;
@@ -1583,10 +1942,14 @@ export class RpgClientEngine<T = any> {
1583
1942
  authoritativeState: PredictionState<Direction>,
1584
1943
  pendingInputs: PredictionHistoryEntry<Direction>[],
1585
1944
  ): void {
1586
- const player = this.getCurrentPlayer();
1945
+ const player = this.getCurrentPlayer() as any;
1587
1946
  if (!player) {
1588
1947
  return;
1589
1948
  }
1949
+ if (!getCanMoveValue(player)) {
1950
+ this.interruptCurrentPlayerMovement(player);
1951
+ return;
1952
+ }
1590
1953
 
1591
1954
  (this.sceneMap as any).stopMovement(player);
1592
1955
  this.applyAuthoritativeState(authoritativeState);
@@ -1699,6 +2062,13 @@ export class RpgClientEngine<T = any> {
1699
2062
  this.resizeHandler = undefined;
1700
2063
  }
1701
2064
 
2065
+ if (this.pointerMoveHandler && this.pointerCanvas) {
2066
+ this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
2067
+ this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
2068
+ this.pointerMoveHandler = undefined;
2069
+ this.pointerCanvas = undefined;
2070
+ }
2071
+
1702
2072
  // Destroy PIXI app and renderer if they exist
1703
2073
  // Destroy the app first, which will destroy the renderer
1704
2074
  // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)