@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
@@ -0,0 +1,324 @@
1
+ import { computed, signal } from "canvasengine";
2
+ import { Hooks } from "@rpgjs/common";
3
+
4
+ export interface ClientProjectileSpawn {
5
+ id: string;
6
+ type: string;
7
+ ownerId?: string;
8
+ origin: { x: number; y: number };
9
+ direction: { x: number; y: number };
10
+ speed: number;
11
+ range: number;
12
+ ttl: number;
13
+ spawnTick: number;
14
+ delay?: number;
15
+ index?: number;
16
+ count?: number;
17
+ params?: Record<string, unknown>;
18
+ collisionMask?: number;
19
+ ignoreOwner?: boolean;
20
+ predictImpact?: boolean;
21
+ }
22
+
23
+ export interface ClientProjectileImpact {
24
+ id: string;
25
+ targetId?: string;
26
+ x: number;
27
+ y: number;
28
+ distance?: number;
29
+ }
30
+
31
+ export interface ClientProjectileDestroy {
32
+ id: string;
33
+ reason?: string;
34
+ targetId?: string;
35
+ x?: number;
36
+ y?: number;
37
+ distance?: number;
38
+ }
39
+
40
+ export interface RenderedProjectileProps extends ClientProjectileSpawn {
41
+ x: number;
42
+ y: number;
43
+ angle: number;
44
+ distance: number;
45
+ elapsed: number;
46
+ progress: number;
47
+ impact?: ClientProjectileImpact;
48
+ impactElapsed?: number;
49
+ impactProgress?: number;
50
+ destroyed?: boolean;
51
+ }
52
+
53
+ export interface RenderedProjectile {
54
+ id: string;
55
+ type: string;
56
+ component: any;
57
+ props: RenderedProjectileProps;
58
+ }
59
+
60
+ export type ProjectilePredictionResolver = (
61
+ projectile: ClientProjectileSpawn,
62
+ ) => ClientProjectileImpact | null | undefined;
63
+
64
+ export interface ProjectileSpawnClock {
65
+ now?: number;
66
+ currentServerTick?: number;
67
+ tickDurationMs?: number;
68
+ }
69
+
70
+ interface RuntimeProjectile {
71
+ spawn: ClientProjectileSpawn;
72
+ component: any;
73
+ createdAt: number;
74
+ impact?: ClientProjectileImpact;
75
+ visualImpact?: ClientProjectileImpact;
76
+ predictedImpact?: ClientProjectileImpact;
77
+ impactStartedAt?: number;
78
+ destroyAt?: number;
79
+ destroyReason?: string;
80
+ }
81
+
82
+ export class ProjectileManager {
83
+ private readonly components = new Map<string, any>();
84
+ private readonly projectiles = new Map<string, RuntimeProjectile>();
85
+ private readonly version = signal(0);
86
+ private readonly impactDurationMs = 350;
87
+
88
+ constructor(
89
+ private readonly hooks: Hooks,
90
+ private readonly predictionResolver?: ProjectilePredictionResolver,
91
+ ) {}
92
+
93
+ current = computed<RenderedProjectile[]>(() => {
94
+ this.version();
95
+ const now = Date.now();
96
+ const rendered: RenderedProjectile[] = [];
97
+ for (const projectile of this.projectiles.values()) {
98
+ const props = this.toProps(projectile, now);
99
+ if (!props) {
100
+ continue;
101
+ }
102
+ rendered.push({
103
+ id: projectile.spawn.id,
104
+ type: projectile.spawn.type,
105
+ component: projectile.component,
106
+ props,
107
+ });
108
+ }
109
+ return rendered;
110
+ });
111
+
112
+ register(type: string, component: any): any {
113
+ this.components.set(type, component);
114
+ return component;
115
+ }
116
+
117
+ get(type: string): any {
118
+ return this.components.get(type);
119
+ }
120
+
121
+ spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {
122
+ const now = clock.now ?? Date.now();
123
+ for (const projectile of projectiles) {
124
+ const component = this.components.get(projectile.type);
125
+ if (!component) {
126
+ continue;
127
+ }
128
+ const runtime: RuntimeProjectile = {
129
+ spawn: {
130
+ ...projectile,
131
+ delay: projectile.delay ?? 0,
132
+ index: projectile.index ?? 0,
133
+ count: projectile.count ?? 1,
134
+ },
135
+ component,
136
+ createdAt: now,
137
+ };
138
+ this.setPredictedImpact(runtime);
139
+ this.projectiles.set(projectile.id, runtime);
140
+ this.hooks.callHooks("client-projectiles-onSpawn", runtime.spawn).subscribe();
141
+ }
142
+ this.touch();
143
+ }
144
+
145
+ impactBatch(impacts: ClientProjectileImpact[]): void {
146
+ const now = Date.now();
147
+ for (const impact of impacts) {
148
+ const projectile = this.projectiles.get(impact.id);
149
+ if (!projectile) {
150
+ continue;
151
+ }
152
+ this.setImpact(projectile, impact, now);
153
+ this.hooks.callHooks("client-projectiles-onImpact", this.toProps(projectile, now)).subscribe();
154
+ }
155
+ this.touch();
156
+ }
157
+
158
+ destroyBatch(projectiles: ClientProjectileDestroy[]): void {
159
+ const now = Date.now();
160
+ for (const destroyed of projectiles) {
161
+ const projectile = this.projectiles.get(destroyed.id);
162
+ if (!projectile) {
163
+ continue;
164
+ }
165
+ if (destroyed.reason === "hit") {
166
+ const current = this.toProps(projectile, now);
167
+ this.setImpact(projectile, {
168
+ id: destroyed.id,
169
+ targetId: destroyed.targetId ?? projectile.impact?.targetId,
170
+ x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,
171
+ y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,
172
+ distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance,
173
+ }, now);
174
+ }
175
+ projectile.destroyReason = destroyed.reason;
176
+ projectile.destroyAt = projectile.destroyAt ?? (
177
+ projectile.impact && projectile.impactStartedAt !== undefined
178
+ ? projectile.impactStartedAt + this.impactDurationMs
179
+ : now
180
+ );
181
+ this.hooks.callHooks("client-projectiles-onDestroy", this.toProps(projectile, now)).subscribe();
182
+ }
183
+ this.touch();
184
+ }
185
+
186
+ clear(): void {
187
+ this.projectiles.clear();
188
+ this.touch();
189
+ }
190
+
191
+ step(): void {
192
+ const now = Date.now();
193
+ let changed = false;
194
+ for (const [id, projectile] of this.projectiles) {
195
+ const props = this.toProps(projectile, now);
196
+ if (
197
+ (!props && !this.isWaitingForDelay(projectile, now)) ||
198
+ (projectile.destroyAt !== undefined && now >= projectile.destroyAt)
199
+ ) {
200
+ this.projectiles.delete(id);
201
+ changed = true;
202
+ }
203
+ }
204
+ this.touch(changed || this.projectiles.size > 0);
205
+ }
206
+
207
+ private toProps(projectile: RuntimeProjectile, now: number): RenderedProjectileProps | null {
208
+ const spawn = projectile.spawn;
209
+ const delayMs = (spawn.delay ?? 0) * 1000;
210
+ const elapsedMs = now - projectile.createdAt - delayMs;
211
+ if (elapsedMs < 0) {
212
+ return null;
213
+ }
214
+ const elapsed = elapsedMs / 1000;
215
+ const ttl = Math.max(0.001, spawn.ttl);
216
+ const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);
217
+ const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);
218
+ const visualImpact = projectile.visualImpact ?? projectile.impact;
219
+ const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;
220
+ const progress = Math.min(1, distance / spawn.range);
221
+ const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;
222
+ const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;
223
+ const impactElapsedMs = projectile.impactStartedAt !== undefined
224
+ ? Math.max(0, now - projectile.impactStartedAt)
225
+ : undefined;
226
+ return {
227
+ ...spawn,
228
+ x,
229
+ y,
230
+ angle: Math.atan2(spawn.direction.y, spawn.direction.x),
231
+ distance,
232
+ elapsed,
233
+ progress,
234
+ impact: projectile.impact,
235
+ impactElapsed: impactElapsedMs === undefined ? undefined : impactElapsedMs / 1000,
236
+ impactProgress: impactElapsedMs === undefined
237
+ ? undefined
238
+ : Math.min(1, impactElapsedMs / this.impactDurationMs),
239
+ destroyed: projectile.destroyAt !== undefined,
240
+ ttl,
241
+ };
242
+ }
243
+
244
+ private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {
245
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
246
+ return now - projectile.createdAt - delayMs < 0;
247
+ }
248
+
249
+ private setPredictedImpact(projectile: RuntimeProjectile): void {
250
+ if (projectile.spawn.predictImpact === false) {
251
+ return;
252
+ }
253
+ const impact = this.predictionResolver?.(projectile.spawn);
254
+ if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) {
255
+ return;
256
+ }
257
+ const distance = typeof impact.distance === "number" && Number.isFinite(impact.distance)
258
+ ? impact.distance
259
+ : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);
260
+ if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) {
261
+ return;
262
+ }
263
+ projectile.predictedImpact = {
264
+ ...impact,
265
+ distance,
266
+ };
267
+ }
268
+
269
+ private getActivePredictedImpact(
270
+ projectile: RuntimeProjectile,
271
+ now: number,
272
+ rawDistance: number,
273
+ ): ClientProjectileImpact | undefined {
274
+ if (!projectile.predictedImpact || projectile.impact) {
275
+ return undefined;
276
+ }
277
+ const distance = projectile.predictedImpact.distance;
278
+ if (distance === undefined || rawDistance < distance) {
279
+ return undefined;
280
+ }
281
+ return projectile.predictedImpact;
282
+ }
283
+
284
+ private setImpact(projectile: RuntimeProjectile, impact: ClientProjectileImpact, now: number): void {
285
+ projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);
286
+ projectile.impact = impact;
287
+ projectile.predictedImpact = undefined;
288
+ projectile.impactStartedAt = projectile.impactStartedAt ?? now;
289
+ const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;
290
+ projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);
291
+ }
292
+
293
+ private resolveVisualImpact(
294
+ projectile: RuntimeProjectile,
295
+ impact: ClientProjectileImpact,
296
+ now: number,
297
+ ): ClientProjectileImpact {
298
+ const predicted = projectile.predictedImpact;
299
+ if (!predicted || !this.isSameTarget(predicted, impact)) {
300
+ return impact;
301
+ }
302
+ const distance = predicted.distance;
303
+ if (distance === undefined) {
304
+ return impact;
305
+ }
306
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
307
+ const elapsedMs = now - projectile.createdAt - delayMs;
308
+ if (elapsedMs < 0) {
309
+ return impact;
310
+ }
311
+ const rawDistance = Math.min(projectile.spawn.speed * (elapsedMs / 1000), projectile.spawn.range);
312
+ return rawDistance >= distance ? predicted : impact;
313
+ }
314
+
315
+ private isSameTarget(a: ClientProjectileImpact, b: ClientProjectileImpact): boolean {
316
+ return a.targetId !== undefined && a.targetId === b.targetId;
317
+ }
318
+
319
+ private touch(force = true): void {
320
+ if (force) {
321
+ this.version.update((value) => value + 1);
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,273 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { Context, injector } from "@signe/di";
3
+ import { signal } from "canvasengine";
4
+ import { PrebuiltGui } from "@rpgjs/common";
5
+ import { WebSocketToken } from "../services/AbstractSocket";
6
+
7
+ vi.mock("../components/gui", () => {
8
+ const component = () => null;
9
+ return {
10
+ DialogboxComponent: component,
11
+ ShopComponent: component,
12
+ SaveLoadComponent: component,
13
+ MainMenuComponent: component,
14
+ NotificationComponent: component,
15
+ TitleScreenComponent: component,
16
+ GameoverComponent: component,
17
+ };
18
+ });
19
+
20
+ const createGui = async () => {
21
+ const { RpgGui } = await import("./Gui");
22
+ const context = new Context();
23
+ const socket = {
24
+ on: vi.fn(),
25
+ emit: vi.fn(),
26
+ };
27
+ await injector(context, [
28
+ {
29
+ provide: WebSocketToken,
30
+ useValue: socket,
31
+ },
32
+ ]);
33
+ return {
34
+ gui: new RpgGui(context),
35
+ socket,
36
+ };
37
+ };
38
+
39
+ const CanvasGui = () => null;
40
+ const VueInventory = {
41
+ name: "inventory",
42
+ render() {
43
+ return null;
44
+ },
45
+ };
46
+ const VueDialog = {
47
+ name: PrebuiltGui.Dialog,
48
+ render() {
49
+ return null;
50
+ },
51
+ };
52
+ const VueMainMenu = {
53
+ name: PrebuiltGui.MainMenu,
54
+ render() {
55
+ return null;
56
+ },
57
+ };
58
+ const VueTooltip = {
59
+ name: "tooltip",
60
+ rpgAttachToSprite: true,
61
+ render() {
62
+ return null;
63
+ },
64
+ };
65
+
66
+ describe("RpgGui Vue integration", () => {
67
+ test("separates CanvasEngine and Vue GUI registries", async () => {
68
+ const { gui } = await createGui();
69
+
70
+ gui.add({
71
+ id: "canvas-tooltip",
72
+ component: CanvasGui,
73
+ attachToSprite: true,
74
+ });
75
+ gui.add({
76
+ id: "inventory",
77
+ component: VueInventory,
78
+ });
79
+ gui.add(VueTooltip);
80
+
81
+ expect(gui.get("canvas-tooltip")?.component).toBe(CanvasGui);
82
+ expect(gui.get("inventory")?.component).toBe(VueInventory);
83
+ expect(gui.get("tooltip")?.component).toBe(VueTooltip);
84
+ expect(gui.getAttachedGuis().map(item => item.name)).toEqual(["canvas-tooltip"]);
85
+ expect(gui.getAttachedVueGuis().map(item => item.name)).toEqual(["tooltip"]);
86
+ });
87
+
88
+ test("synchronizes Vue GUI display and hide states through the Vue bridge", async () => {
89
+ const { gui } = await createGui();
90
+ const bridge = {
91
+ updateGuiState: vi.fn(),
92
+ initializeGuiStates: vi.fn(),
93
+ };
94
+
95
+ gui.add({
96
+ id: "inventory",
97
+ component: VueInventory,
98
+ });
99
+ gui._setVueGuiInstance(bridge);
100
+
101
+ expect(bridge.initializeGuiStates).toHaveBeenCalledWith([
102
+ expect.objectContaining({
103
+ name: "inventory",
104
+ display: false,
105
+ data: {},
106
+ attachToSprite: false,
107
+ }),
108
+ ]);
109
+
110
+ gui.display("inventory", { gold: 12 });
111
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
112
+ expect.objectContaining({
113
+ name: "inventory",
114
+ display: true,
115
+ data: { gold: 12 },
116
+ attachToSprite: false,
117
+ }),
118
+ );
119
+
120
+ gui.hide("inventory");
121
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
122
+ expect.objectContaining({
123
+ name: "inventory",
124
+ display: false,
125
+ }),
126
+ );
127
+ });
128
+
129
+ test("waits for Vue GUI dependencies before display", async () => {
130
+ const { gui } = await createGui();
131
+ const bridge = {
132
+ updateGuiState: vi.fn(),
133
+ initializeGuiStates: vi.fn(),
134
+ };
135
+ const dependency = signal<any>(undefined);
136
+
137
+ gui.add({
138
+ id: "inventory",
139
+ component: VueInventory,
140
+ dependencies: () => [dependency],
141
+ });
142
+ gui._setVueGuiInstance(bridge);
143
+ gui.display("inventory", { items: ["potion"] });
144
+
145
+ expect(gui.isDisplaying("inventory")).toBe(false);
146
+ expect(bridge.updateGuiState).not.toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ display: true,
149
+ }),
150
+ );
151
+
152
+ dependency.set({ id: "player" });
153
+
154
+ expect(gui.isDisplaying("inventory")).toBe(true);
155
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
156
+ expect.objectContaining({
157
+ name: "inventory",
158
+ display: true,
159
+ data: { items: ["potion"] },
160
+ }),
161
+ );
162
+ });
163
+
164
+ test("allows Vue GUI entries to replace prebuilt CanvasEngine GUIs", async () => {
165
+ const { gui } = await createGui();
166
+ const bridge = {
167
+ updateGuiState: vi.fn(),
168
+ initializeGuiStates: vi.fn(),
169
+ };
170
+
171
+ gui._setVueGuiInstance(bridge);
172
+ gui.add({
173
+ id: PrebuiltGui.Dialog,
174
+ component: VueDialog,
175
+ });
176
+
177
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(VueDialog);
178
+ expect(gui.getAll()[PrebuiltGui.Dialog].component).toBe(VueDialog);
179
+ expect((gui as any).gui()[PrebuiltGui.Dialog]).toBeUndefined();
180
+ expect(gui.getVueGuis().filter(item => item.name === PrebuiltGui.Dialog)).toHaveLength(1);
181
+
182
+ gui.display(PrebuiltGui.Dialog, { text: "Hello" });
183
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
184
+ expect.objectContaining({
185
+ name: PrebuiltGui.Dialog,
186
+ display: true,
187
+ data: { text: "Hello" },
188
+ }),
189
+ );
190
+
191
+ gui.hide(PrebuiltGui.Dialog);
192
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
193
+ expect.objectContaining({
194
+ name: PrebuiltGui.Dialog,
195
+ display: false,
196
+ }),
197
+ );
198
+ });
199
+
200
+ test("allows CanvasEngine GUI entries to replace Vue GUI entries with the same id", async () => {
201
+ const { gui } = await createGui();
202
+ const bridge = {
203
+ updateGuiState: vi.fn(),
204
+ initializeGuiStates: vi.fn(),
205
+ };
206
+
207
+ gui._setVueGuiInstance(bridge);
208
+ gui.add({
209
+ id: PrebuiltGui.Dialog,
210
+ component: VueDialog,
211
+ });
212
+ gui.add({
213
+ id: PrebuiltGui.Dialog,
214
+ component: CanvasGui,
215
+ });
216
+
217
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(CanvasGui);
218
+ expect(gui.getVueGuis().some(item => item.name === PrebuiltGui.Dialog)).toBe(false);
219
+ expect((gui as any).gui()[PrebuiltGui.Dialog].component).toBe(CanvasGui);
220
+ expect(bridge.initializeGuiStates).toHaveBeenLastCalledWith([]);
221
+ });
222
+
223
+ test("keeps main menu optimistic reducers when a Vue GUI replaces the prebuilt component", async () => {
224
+ const { gui, socket } = await createGui();
225
+ const bridge = {
226
+ updateGuiState: vi.fn(),
227
+ initializeGuiStates: vi.fn(),
228
+ };
229
+
230
+ gui.add({
231
+ id: PrebuiltGui.MainMenu,
232
+ component: VueMainMenu,
233
+ });
234
+ gui._setVueGuiInstance(bridge);
235
+ gui.display(PrebuiltGui.MainMenu, {
236
+ items: [
237
+ {
238
+ id: "potion",
239
+ quantity: 2,
240
+ },
241
+ ],
242
+ });
243
+
244
+ gui.guiInteraction(PrebuiltGui.MainMenu, "useItem", { id: "potion" });
245
+
246
+ expect(gui.get(PrebuiltGui.MainMenu)?.data().items).toEqual([
247
+ {
248
+ id: "potion",
249
+ quantity: 1,
250
+ },
251
+ ]);
252
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
253
+ expect.objectContaining({
254
+ name: PrebuiltGui.MainMenu,
255
+ data: {
256
+ items: [
257
+ {
258
+ id: "potion",
259
+ quantity: 1,
260
+ },
261
+ ],
262
+ },
263
+ }),
264
+ );
265
+ expect(socket.emit).toHaveBeenCalledWith(
266
+ "gui.interaction",
267
+ expect.objectContaining({
268
+ guiId: PrebuiltGui.MainMenu,
269
+ name: "useItem",
270
+ }),
271
+ );
272
+ });
273
+ });