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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/Game/AnimationManager.d.ts +1 -0
  3. package/dist/Game/AnimationManager.js +3 -0
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/ClientVisuals.d.ts +61 -0
  6. package/dist/Game/ClientVisuals.js +96 -0
  7. package/dist/Game/ClientVisuals.js.map +1 -0
  8. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  9. package/dist/Game/EventComponentResolver.d.ts +16 -0
  10. package/dist/Game/EventComponentResolver.js +52 -0
  11. package/dist/Game/EventComponentResolver.js.map +1 -0
  12. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  13. package/dist/Game/Map.js +9 -0
  14. package/dist/Game/Map.js.map +1 -1
  15. package/dist/Game/Object.js +2 -2
  16. package/dist/Game/Object.js.map +1 -1
  17. package/dist/Game/Object.spec.d.ts +1 -0
  18. package/dist/Game/ProjectileManager.d.ts +98 -0
  19. package/dist/Game/ProjectileManager.js +196 -0
  20. package/dist/Game/ProjectileManager.js.map +1 -0
  21. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  22. package/dist/RpgClient.d.ts +117 -13
  23. package/dist/RpgClientEngine.d.ts +82 -4
  24. package/dist/RpgClientEngine.js +296 -51
  25. package/dist/RpgClientEngine.js.map +1 -1
  26. package/dist/components/animations/fx.ce.js +58 -0
  27. package/dist/components/animations/fx.ce.js.map +1 -0
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/animations/index.d.ts +1 -0
  30. package/dist/components/animations/index.js +3 -1
  31. package/dist/components/animations/index.js.map +1 -1
  32. package/dist/components/character.ce.js +140 -40
  33. package/dist/components/character.ce.js.map +1 -1
  34. package/dist/components/dynamics/bar.ce.js +4 -3
  35. package/dist/components/dynamics/bar.ce.js.map +1 -1
  36. package/dist/components/dynamics/image.ce.js +2 -1
  37. package/dist/components/dynamics/image.ce.js.map +1 -1
  38. package/dist/components/dynamics/shape.ce.js +3 -2
  39. package/dist/components/dynamics/shape.ce.js.map +1 -1
  40. package/dist/components/dynamics/text.ce.js +9 -8
  41. package/dist/components/dynamics/text.ce.js.map +1 -1
  42. package/dist/components/gui/dialogbox/index.ce.js +3 -2
  43. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  44. package/dist/components/gui/gameover.ce.js +3 -2
  45. package/dist/components/gui/gameover.ce.js.map +1 -1
  46. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  47. package/dist/components/gui/menu/equip-menu.ce.js +2 -1
  48. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  49. package/dist/components/gui/menu/exit-menu.ce.js +2 -1
  50. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  51. package/dist/components/gui/menu/items-menu.ce.js +3 -2
  52. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/main-menu.ce.js +3 -2
  54. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  57. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  58. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  59. package/dist/components/gui/save-load.ce.js +2 -1
  60. package/dist/components/gui/save-load.ce.js.map +1 -1
  61. package/dist/components/gui/shop/shop.ce.js +3 -2
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js +3 -2
  64. package/dist/components/gui/title-screen.ce.js.map +1 -1
  65. package/dist/components/index.d.ts +2 -1
  66. package/dist/components/index.js +1 -0
  67. package/dist/components/player-components.ce.js +11 -10
  68. package/dist/components/player-components.ce.js.map +1 -1
  69. package/dist/components/prebuilt/hp-bar.ce.js +4 -3
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js +2 -1
  72. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  73. package/dist/components/scenes/canvas.ce.js +12 -4
  74. package/dist/components/scenes/canvas.ce.js.map +1 -1
  75. package/dist/components/scenes/draw-map.ce.js +6 -3
  76. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  77. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  78. package/dist/index.d.ts +4 -0
  79. package/dist/index.js +10 -5
  80. package/dist/module.js +18 -0
  81. package/dist/module.js.map +1 -1
  82. package/dist/services/actionInput.d.ts +14 -0
  83. package/dist/services/actionInput.js +59 -0
  84. package/dist/services/actionInput.js.map +1 -0
  85. package/dist/services/actionInput.spec.d.ts +1 -0
  86. package/dist/services/mmorpg-connection.d.ts +5 -0
  87. package/dist/services/mmorpg-connection.js +50 -0
  88. package/dist/services/mmorpg-connection.js.map +1 -0
  89. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  90. package/dist/services/mmorpg.d.ts +10 -4
  91. package/dist/services/mmorpg.js +48 -30
  92. package/dist/services/mmorpg.js.map +1 -1
  93. package/dist/services/pointerContext.d.ts +11 -0
  94. package/dist/services/pointerContext.js +48 -0
  95. package/dist/services/pointerContext.js.map +1 -0
  96. package/dist/services/pointerContext.spec.d.ts +1 -0
  97. package/dist/services/standalone-message.d.ts +1 -0
  98. package/dist/services/standalone-message.js +9 -0
  99. package/dist/services/standalone-message.js.map +1 -0
  100. package/dist/services/standalone.d.ts +3 -1
  101. package/dist/services/standalone.js +34 -15
  102. package/dist/services/standalone.js.map +1 -1
  103. package/dist/services/standalone.spec.d.ts +1 -0
  104. package/dist/utils/mapId.d.ts +1 -0
  105. package/dist/utils/mapId.js +6 -0
  106. package/dist/utils/mapId.js.map +1 -0
  107. package/package.json +7 -7
  108. package/src/Game/AnimationManager.ts +4 -0
  109. package/src/Game/ClientVisuals.spec.ts +56 -0
  110. package/src/Game/ClientVisuals.ts +184 -0
  111. package/src/Game/EventComponentResolver.spec.ts +84 -0
  112. package/src/Game/EventComponentResolver.ts +74 -0
  113. package/src/Game/Map.ts +10 -0
  114. package/src/Game/Object.spec.ts +46 -0
  115. package/src/Game/Object.ts +2 -2
  116. package/src/Game/ProjectileManager.spec.ts +449 -0
  117. package/src/Game/ProjectileManager.ts +346 -0
  118. package/src/RpgClient.ts +130 -15
  119. package/src/RpgClientEngine.ts +405 -69
  120. package/src/components/animations/fx.ce +101 -0
  121. package/src/components/animations/index.ts +4 -2
  122. package/src/components/character.ce +185 -40
  123. package/src/components/dynamics/bar.ce +4 -3
  124. package/src/components/dynamics/image.ce +2 -1
  125. package/src/components/dynamics/shape.ce +3 -2
  126. package/src/components/dynamics/text.ce +9 -8
  127. package/src/components/gui/dialogbox/index.ce +3 -2
  128. package/src/components/gui/gameover.ce +2 -1
  129. package/src/components/gui/menu/equip-menu.ce +2 -1
  130. package/src/components/gui/menu/exit-menu.ce +2 -1
  131. package/src/components/gui/menu/items-menu.ce +3 -2
  132. package/src/components/gui/menu/main-menu.ce +2 -1
  133. package/src/components/gui/save-load.ce +2 -1
  134. package/src/components/gui/shop/shop.ce +3 -2
  135. package/src/components/gui/title-screen.ce +2 -1
  136. package/src/components/index.ts +2 -1
  137. package/src/components/player-components.ce +11 -10
  138. package/src/components/prebuilt/hp-bar.ce +4 -3
  139. package/src/components/prebuilt/light-halo.ce +2 -2
  140. package/src/components/scenes/canvas.ce +10 -2
  141. package/src/components/scenes/draw-map.ce +17 -3
  142. package/src/index.ts +4 -0
  143. package/src/module.ts +24 -0
  144. package/src/services/actionInput.spec.ts +155 -0
  145. package/src/services/actionInput.ts +120 -0
  146. package/src/services/mmorpg-connection.spec.ts +99 -0
  147. package/src/services/mmorpg-connection.ts +69 -0
  148. package/src/services/mmorpg.ts +60 -34
  149. package/src/services/pointerContext.spec.ts +36 -0
  150. package/src/services/pointerContext.ts +84 -0
  151. package/src/services/standalone-message.ts +7 -0
  152. package/src/services/standalone.spec.ts +34 -0
  153. package/src/services/standalone.ts +42 -12
  154. package/src/utils/mapId.ts +2 -0
@@ -0,0 +1,346 @@
1
+ import { computed, signal } from "canvasengine";
2
+ import { Hooks } from "@rpgjs/common";
3
+ import { normalizeRoomMapId } from "../utils/mapId";
4
+
5
+ export interface ClientProjectileSpawn {
6
+ id: string;
7
+ type: string;
8
+ ownerId?: string;
9
+ origin: { x: number; y: number };
10
+ direction: { x: number; y: number };
11
+ speed: number;
12
+ range: number;
13
+ ttl: number;
14
+ spawnTick: number;
15
+ delay?: number;
16
+ index?: number;
17
+ count?: number;
18
+ params?: Record<string, unknown>;
19
+ collisionMask?: number;
20
+ ignoreOwner?: boolean;
21
+ predictImpact?: boolean;
22
+ }
23
+
24
+ export interface ClientProjectileImpact {
25
+ id: string;
26
+ targetId?: string;
27
+ x: number;
28
+ y: number;
29
+ distance?: number;
30
+ }
31
+
32
+ export interface ClientProjectileDestroy {
33
+ id: string;
34
+ reason?: string;
35
+ targetId?: string;
36
+ x?: number;
37
+ y?: number;
38
+ distance?: number;
39
+ }
40
+
41
+ export interface RenderedProjectileProps extends ClientProjectileSpawn {
42
+ x: number;
43
+ y: number;
44
+ angle: number;
45
+ distance: number;
46
+ elapsed: number;
47
+ progress: number;
48
+ impact?: ClientProjectileImpact;
49
+ impactElapsed?: number;
50
+ impactProgress?: number;
51
+ destroyed?: boolean;
52
+ }
53
+
54
+ export interface RenderedProjectile {
55
+ id: string;
56
+ type: string;
57
+ component: any;
58
+ props: RenderedProjectileProps;
59
+ }
60
+
61
+ export type ProjectilePredictionResolver = (
62
+ projectile: ClientProjectileSpawn,
63
+ ) => ClientProjectileImpact | null | undefined;
64
+
65
+ export interface ProjectileSpawnClock {
66
+ now?: number;
67
+ currentServerTick?: number;
68
+ tickDurationMs?: number;
69
+ mapId?: string;
70
+ }
71
+
72
+ interface RuntimeProjectile {
73
+ spawn: ClientProjectileSpawn;
74
+ component: any;
75
+ createdAt: number;
76
+ impact?: ClientProjectileImpact;
77
+ visualImpact?: ClientProjectileImpact;
78
+ predictedImpact?: ClientProjectileImpact;
79
+ impactStartedAt?: number;
80
+ destroyAt?: number;
81
+ destroyReason?: string;
82
+ }
83
+
84
+ export class ProjectileManager {
85
+ private readonly components = new Map<string, any>();
86
+ private readonly projectiles = new Map<string, RuntimeProjectile>();
87
+ private readonly version = signal(0);
88
+ private readonly impactDurationMs = 350;
89
+ private mapId?: string;
90
+
91
+ constructor(
92
+ private readonly hooks: Hooks,
93
+ private readonly predictionResolver?: ProjectilePredictionResolver,
94
+ ) {}
95
+
96
+ current = computed<RenderedProjectile[]>(() => {
97
+ this.version();
98
+ const now = Date.now();
99
+ const rendered: RenderedProjectile[] = [];
100
+ for (const projectile of this.projectiles.values()) {
101
+ const props = this.toProps(projectile, now);
102
+ if (!props) {
103
+ continue;
104
+ }
105
+ rendered.push({
106
+ id: projectile.spawn.id,
107
+ type: projectile.spawn.type,
108
+ component: projectile.component,
109
+ props,
110
+ });
111
+ }
112
+ return rendered;
113
+ });
114
+
115
+ register(type: string, component: any): any {
116
+ this.components.set(type, component);
117
+ return component;
118
+ }
119
+
120
+ get(type: string): any {
121
+ return this.components.get(type);
122
+ }
123
+
124
+ setMapId(mapId: string | undefined): void {
125
+ const normalizedMapId = normalizeRoomMapId(mapId);
126
+ if (this.mapId === normalizedMapId) return;
127
+ this.mapId = normalizedMapId;
128
+ this.clear();
129
+ }
130
+
131
+ getMapId(): string | undefined {
132
+ return this.mapId;
133
+ }
134
+
135
+ spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {
136
+ if (!this.acceptsMap(clock.mapId)) return;
137
+ const now = clock.now ?? Date.now();
138
+ for (const projectile of projectiles) {
139
+ const component = this.components.get(projectile.type);
140
+ if (!component) {
141
+ continue;
142
+ }
143
+ const runtime: RuntimeProjectile = {
144
+ spawn: {
145
+ ...projectile,
146
+ delay: projectile.delay ?? 0,
147
+ index: projectile.index ?? 0,
148
+ count: projectile.count ?? 1,
149
+ },
150
+ component,
151
+ createdAt: now,
152
+ };
153
+ this.setPredictedImpact(runtime);
154
+ this.projectiles.set(projectile.id, runtime);
155
+ this.hooks.callHooks("client-projectiles-onSpawn", runtime.spawn).subscribe();
156
+ }
157
+ this.touch();
158
+ }
159
+
160
+ impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {
161
+ if (!this.acceptsMap(context.mapId)) return;
162
+ const now = Date.now();
163
+ for (const impact of impacts) {
164
+ const projectile = this.projectiles.get(impact.id);
165
+ if (!projectile) {
166
+ continue;
167
+ }
168
+ this.setImpact(projectile, impact, now);
169
+ this.hooks.callHooks("client-projectiles-onImpact", this.toProps(projectile, now)).subscribe();
170
+ }
171
+ this.touch();
172
+ }
173
+
174
+ destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {
175
+ if (!this.acceptsMap(context.mapId)) return;
176
+ const now = Date.now();
177
+ for (const destroyed of projectiles) {
178
+ const projectile = this.projectiles.get(destroyed.id);
179
+ if (!projectile) {
180
+ continue;
181
+ }
182
+ if (destroyed.reason === "hit") {
183
+ const current = this.toProps(projectile, now);
184
+ this.setImpact(projectile, {
185
+ id: destroyed.id,
186
+ targetId: destroyed.targetId ?? projectile.impact?.targetId,
187
+ x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,
188
+ y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,
189
+ distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance,
190
+ }, now);
191
+ }
192
+ projectile.destroyReason = destroyed.reason;
193
+ projectile.destroyAt = projectile.destroyAt ?? (
194
+ projectile.impact && projectile.impactStartedAt !== undefined
195
+ ? projectile.impactStartedAt + this.impactDurationMs
196
+ : now
197
+ );
198
+ this.hooks.callHooks("client-projectiles-onDestroy", this.toProps(projectile, now)).subscribe();
199
+ }
200
+ this.touch();
201
+ }
202
+
203
+ clear(): void {
204
+ this.projectiles.clear();
205
+ this.touch();
206
+ }
207
+
208
+ step(): void {
209
+ const now = Date.now();
210
+ let changed = false;
211
+ for (const [id, projectile] of this.projectiles) {
212
+ const props = this.toProps(projectile, now);
213
+ if (
214
+ (!props && !this.isWaitingForDelay(projectile, now)) ||
215
+ (projectile.destroyAt !== undefined && now >= projectile.destroyAt)
216
+ ) {
217
+ this.projectiles.delete(id);
218
+ changed = true;
219
+ }
220
+ }
221
+ this.touch(changed || this.projectiles.size > 0);
222
+ }
223
+
224
+ private toProps(projectile: RuntimeProjectile, now: number): RenderedProjectileProps | null {
225
+ const spawn = projectile.spawn;
226
+ const delayMs = (spawn.delay ?? 0) * 1000;
227
+ const elapsedMs = now - projectile.createdAt - delayMs;
228
+ if (elapsedMs < 0) {
229
+ return null;
230
+ }
231
+ const elapsed = elapsedMs / 1000;
232
+ const ttl = Math.max(0.001, spawn.ttl);
233
+ const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);
234
+ const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);
235
+ const visualImpact = projectile.visualImpact ?? projectile.impact;
236
+ const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;
237
+ const progress = Math.min(1, distance / spawn.range);
238
+ const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;
239
+ const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;
240
+ const impactElapsedMs = projectile.impactStartedAt !== undefined
241
+ ? Math.max(0, now - projectile.impactStartedAt)
242
+ : undefined;
243
+ return {
244
+ ...spawn,
245
+ x,
246
+ y,
247
+ angle: Math.atan2(spawn.direction.y, spawn.direction.x),
248
+ distance,
249
+ elapsed,
250
+ progress,
251
+ impact: projectile.impact,
252
+ impactElapsed: impactElapsedMs === undefined ? undefined : impactElapsedMs / 1000,
253
+ impactProgress: impactElapsedMs === undefined
254
+ ? undefined
255
+ : Math.min(1, impactElapsedMs / this.impactDurationMs),
256
+ destroyed: projectile.destroyAt !== undefined,
257
+ ttl,
258
+ };
259
+ }
260
+
261
+ private acceptsMap(mapId: string | undefined): boolean {
262
+ const normalizedMapId = normalizeRoomMapId(mapId);
263
+ return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
264
+ }
265
+
266
+ private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {
267
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
268
+ return now - projectile.createdAt - delayMs < 0;
269
+ }
270
+
271
+ private setPredictedImpact(projectile: RuntimeProjectile): void {
272
+ if (projectile.spawn.predictImpact === false) {
273
+ return;
274
+ }
275
+ const impact = this.predictionResolver?.(projectile.spawn);
276
+ if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) {
277
+ return;
278
+ }
279
+ const distance = typeof impact.distance === "number" && Number.isFinite(impact.distance)
280
+ ? impact.distance
281
+ : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);
282
+ if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) {
283
+ return;
284
+ }
285
+ projectile.predictedImpact = {
286
+ ...impact,
287
+ distance,
288
+ };
289
+ }
290
+
291
+ private getActivePredictedImpact(
292
+ projectile: RuntimeProjectile,
293
+ now: number,
294
+ rawDistance: number,
295
+ ): ClientProjectileImpact | undefined {
296
+ if (!projectile.predictedImpact || projectile.impact) {
297
+ return undefined;
298
+ }
299
+ const distance = projectile.predictedImpact.distance;
300
+ if (distance === undefined || rawDistance < distance) {
301
+ return undefined;
302
+ }
303
+ return projectile.predictedImpact;
304
+ }
305
+
306
+ private setImpact(projectile: RuntimeProjectile, impact: ClientProjectileImpact, now: number): void {
307
+ projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);
308
+ projectile.impact = impact;
309
+ projectile.predictedImpact = undefined;
310
+ projectile.impactStartedAt = projectile.impactStartedAt ?? now;
311
+ const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;
312
+ projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);
313
+ }
314
+
315
+ private resolveVisualImpact(
316
+ projectile: RuntimeProjectile,
317
+ impact: ClientProjectileImpact,
318
+ now: number,
319
+ ): ClientProjectileImpact {
320
+ const predicted = projectile.predictedImpact;
321
+ if (!predicted || !this.isSameTarget(predicted, impact)) {
322
+ return impact;
323
+ }
324
+ const distance = predicted.distance;
325
+ if (distance === undefined) {
326
+ return impact;
327
+ }
328
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
329
+ const elapsedMs = now - projectile.createdAt - delayMs;
330
+ if (elapsedMs < 0) {
331
+ return impact;
332
+ }
333
+ const rawDistance = Math.min(projectile.spawn.speed * (elapsedMs / 1000), projectile.spawn.range);
334
+ return rawDistance >= distance ? predicted : impact;
335
+ }
336
+
337
+ private isSameTarget(a: ClientProjectileImpact, b: ClientProjectileImpact): boolean {
338
+ return a.targetId !== undefined && a.targetId === b.targetId;
339
+ }
340
+
341
+ private touch(force = true): void {
342
+ if (force) {
343
+ this.version.update((value) => value + 1);
344
+ }
345
+ }
346
+ }
package/src/RpgClient.ts CHANGED
@@ -2,7 +2,13 @@ import { ComponentFunction, Signal } from 'canvasengine'
2
2
  import { RpgClientEngine } from './RpgClientEngine'
3
3
  import { Loader, Container } from 'pixi.js'
4
4
  import { RpgClientObject } from './Game/Object'
5
- import { type MapPhysicsEntityContext, type MapPhysicsInitContext } from '@rpgjs/common'
5
+ import type { RpgClientEvent } from './Game/Event'
6
+ import { type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
7
+ import type {
8
+ ClientProjectileSpawn,
9
+ RenderedProjectileProps,
10
+ } from './Game/ProjectileManager'
11
+ import type { ClientVisualMap } from './Game/ClientVisuals'
6
12
 
7
13
  type RpgClass<T = any> = new (...args: any[]) => T
8
14
  type RpgComponent = RpgClientObject
@@ -14,6 +20,16 @@ export type SpriteComponentConfig = ComponentFunction | {
14
20
  dependencies?: (object: RpgClientObject) => any[]
15
21
  }
16
22
 
23
+ export type EventComponentSprite = RpgClientEvent & Record<string, any>
24
+
25
+ export type EventComponentConfig = ComponentFunction | {
26
+ component: ComponentFunction
27
+ props?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
28
+ data?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
29
+ dependencies?: (event: EventComponentSprite) => any[]
30
+ renderGraphic?: boolean
31
+ }
32
+
17
33
  export interface RpgSpriteBeforeRemoveContext {
18
34
  reason?: string
19
35
  data?: any
@@ -46,13 +62,14 @@ export interface RpgClientEngineHooks {
46
62
  /**
47
63
  * Recover keys from the pressed keyboard
48
64
  *
49
- * @prop { (engine: RpgClientEngine, obj: { input: string, playerId: number }) => any } [onInput]
65
+ * @prop { (engine: RpgClientEngine, obj: { input: string | number, action?: string | number, data?: any, playerId: number }) => any } [onInput]
50
66
  * @memberof RpgEngineHooks
51
67
  */
52
- onInput?: (engine: RpgClientEngine, obj: { input: string, playerId: number }) => any
68
+ onInput?: (engine: RpgClientEngine, obj: { input: RpgActionName, action?: RpgActionName, data?: any, playerId: number }) => any
53
69
 
54
70
  /**
55
- * Called when the user is connected to the server
71
+ * Called when the user is connected to the server. In MMORPG mode, this
72
+ * runs after the server sends the RPGJS connection acceptance packet.
56
73
  *
57
74
  * @prop { (engine: RpgClientEngine, socket: any) => any } [onConnected]
58
75
  * @memberof RpgEngineHooks
@@ -68,7 +85,8 @@ export interface RpgClientEngineHooks {
68
85
  onDisconnect?: (engine: RpgClientEngine, reason: any, socket: any) => any
69
86
 
70
87
  /**
71
- * Called when there was a connection error
88
+ * Called when there was a connection error. In MMORPG mode, this also runs
89
+ * when server-side auth refuses the connection.
72
90
  *
73
91
  * @prop { (engine: RpgClientEngine, err: any, socket: any) => any } [onConnectError]
74
92
  * @memberof RpgEngineHooks
@@ -136,6 +154,31 @@ export interface RpgSpriteHooks {
136
154
  * ```
137
155
  */
138
156
  components?: Record<string, ComponentFunction>
157
+
158
+ /**
159
+ * Resolve a custom CanvasEngine component for a specific event.
160
+ *
161
+ * The component always receives the synced event object as the `sprite` prop.
162
+ * Custom props are merged in addition to `sprite`, but cannot replace it.
163
+ * Return `null` or `undefined` to keep the default graphic renderer.
164
+ *
165
+ * @prop { (event: EventComponentSprite) => EventComponentConfig | null | undefined } [eventComponent]
166
+ * @memberof RpgSpriteHooks
167
+ * @example
168
+ * ```ts
169
+ * import ChestEvent from './components/chest-event.ce'
170
+ *
171
+ * const sprite: RpgSpriteHooks = {
172
+ * eventComponent(sprite) {
173
+ * if (sprite.name === 'CHEST') {
174
+ * return ChestEvent
175
+ * }
176
+ * return null
177
+ * }
178
+ * }
179
+ * ```
180
+ */
181
+ eventComponent?: (event: EventComponentSprite) => EventComponentConfig | null | undefined
139
182
 
140
183
  /**
141
184
  * As soon as the sprite is initialized
@@ -245,6 +288,14 @@ export interface RpgSceneHooks<Scene> {
245
288
  }
246
289
 
247
290
  export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
291
+ /**
292
+ * Root CanvasEngine component used to render the RPG scene map.
293
+ *
294
+ * Use the exported `SceneMap` component inside your custom component to
295
+ * keep the default map rendering and compose additional scene children.
296
+ */
297
+ component?: ComponentFunction
298
+
248
299
  /**
249
300
  * The map and resources are being loaded
250
301
  *
@@ -289,6 +340,28 @@ export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
289
340
  onPhysicsReset?: (scene: SceneMap) => any
290
341
  }
291
342
 
343
+ export interface RpgProjectileHooks {
344
+ /**
345
+ * CanvasEngine components used to render server-authoritative projectiles.
346
+ */
347
+ components?: Record<string, ComponentFunction>
348
+
349
+ /**
350
+ * Called when a projectile spawn batch is received from the server.
351
+ */
352
+ onSpawn?: (projectile: ClientProjectileSpawn) => any
353
+
354
+ /**
355
+ * Called when the server confirms a projectile impact.
356
+ */
357
+ onImpact?: (projectile: RenderedProjectileProps | null) => any
358
+
359
+ /**
360
+ * Called when the server destroys a projectile.
361
+ */
362
+ onDestroy?: (projectile: RenderedProjectileProps | null) => any
363
+ }
364
+
292
365
  export interface RpgClient {
293
366
  /**
294
367
  * Add hooks to the player or engine. All modules can listen to the hook
@@ -624,33 +697,36 @@ export interface RpgClient {
624
697
  * */
625
698
  sprite?: RpgSpriteHooks
626
699
 
627
- /**
628
- * Reference the scenes of the game. Here you can put your own class that inherits RpgSceneMap
700
+ /**
701
+ * Reference the scenes of the game.
629
702
  *
630
703
  * ```ts
631
704
  * import { RpgSceneMapHooks, RpgClient, defineModule } from '@rpgjs/client'
705
+ * import MyScene from './my-scene.ce'
632
706
  *
633
707
  * export const sceneMap: RpgSceneMapHooks = {
634
- *
708
+ * component: MyScene
635
709
  * }
636
710
  *
637
711
  * defineModule<RpgClient>({
638
- * scenes: {
639
- * // If you put the RpgSceneMap scene, Thhe key is called mandatory `map`
640
- * map: sceneMap
641
- * }
712
+ * sceneMap
642
713
  * })
643
714
  * ```
644
715
  *
645
- * @prop { [sceneName: string]: RpgSceneMapHooks } [scenes]
716
+ * @prop {RpgSceneMapHooks} [sceneMap]
646
717
  * @memberof RpgClient
647
718
  * */
719
+ sceneMap?: RpgSceneMapHooks
720
+
721
+ /**
722
+ * Legacy scene map hook container.
723
+ *
724
+ * Prefer `sceneMap` for new code.
725
+ */
648
726
  scenes?: {
649
727
  map: RpgSceneMapHooks
650
728
  }
651
729
 
652
- sceneMap?: RpgSceneMapHooks
653
-
654
730
  /**
655
731
  * Array containing the list of component animations
656
732
  * Each element defines a temporary component to display for animations like hits, effects, etc.
@@ -681,4 +757,43 @@ export interface RpgClient {
681
757
  id: string,
682
758
  component: ComponentFunction
683
759
  }[]
760
+
761
+ /**
762
+ * Named client-side visual macros.
763
+ *
764
+ * Use client visuals when the server needs to trigger a group of existing
765
+ * client visual primitives at once, such as a flash, damage text, sound,
766
+ * component animation, and camera shake. The server sends only the visual
767
+ * name and a serializable payload; the rendering details live on the client.
768
+ *
769
+ * For a single sound, flash, or component animation, prefer the direct
770
+ * server APIs (`playSound`, `flash`, `showComponentAnimation`). Client
771
+ * visuals are meant to group several visual operations and reduce bandwidth.
772
+ *
773
+ * ```ts
774
+ * import { defineModule, RpgClient } from '@rpgjs/client'
775
+ *
776
+ * export default defineModule<RpgClient>({
777
+ * clientVisuals: {
778
+ * hit({ target, data }, helpers) {
779
+ * helpers.flash(target, { type: 'tint', tint: 'red' })
780
+ * helpers.showHit(target, `-${data.damage}`)
781
+ * helpers.sound('hit')
782
+ * }
783
+ * }
784
+ * })
785
+ * ```
786
+ *
787
+ * @prop {Record<string, ClientVisualHandler>} [clientVisuals]
788
+ * @memberof RpgClient
789
+ */
790
+ clientVisuals?: ClientVisualMap
791
+
792
+ /**
793
+ * Client-side projectile rendering configuration.
794
+ *
795
+ * Register a CanvasEngine component per projectile type. The server sends
796
+ * compact spawn/impact/destroy events and the client predicts x/y locally.
797
+ */
798
+ projectiles?: RpgProjectileHooks
684
799
  }