@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,196 @@
1
+ import { normalizeRoomMapId } from "../utils/mapId.js";
2
+ import { computed, signal } from "canvasengine";
3
+ //#region src/Game/ProjectileManager.ts
4
+ var ProjectileManager = class {
5
+ constructor(hooks, predictionResolver) {
6
+ this.hooks = hooks;
7
+ this.predictionResolver = predictionResolver;
8
+ this.components = /* @__PURE__ */ new Map();
9
+ this.projectiles = /* @__PURE__ */ new Map();
10
+ this.version = signal(0);
11
+ this.impactDurationMs = 350;
12
+ this.current = computed(() => {
13
+ this.version();
14
+ const now = Date.now();
15
+ const rendered = [];
16
+ for (const projectile of this.projectiles.values()) {
17
+ const props = this.toProps(projectile, now);
18
+ if (!props) continue;
19
+ rendered.push({
20
+ id: projectile.spawn.id,
21
+ type: projectile.spawn.type,
22
+ component: projectile.component,
23
+ props
24
+ });
25
+ }
26
+ return rendered;
27
+ });
28
+ }
29
+ register(type, component) {
30
+ this.components.set(type, component);
31
+ return component;
32
+ }
33
+ get(type) {
34
+ return this.components.get(type);
35
+ }
36
+ setMapId(mapId) {
37
+ const normalizedMapId = normalizeRoomMapId(mapId);
38
+ if (this.mapId === normalizedMapId) return;
39
+ this.mapId = normalizedMapId;
40
+ this.clear();
41
+ }
42
+ getMapId() {
43
+ return this.mapId;
44
+ }
45
+ spawnBatch(projectiles, clock = {}) {
46
+ if (!this.acceptsMap(clock.mapId)) return;
47
+ const now = clock.now ?? Date.now();
48
+ for (const projectile of projectiles) {
49
+ const component = this.components.get(projectile.type);
50
+ if (!component) continue;
51
+ const runtime = {
52
+ spawn: {
53
+ ...projectile,
54
+ delay: projectile.delay ?? 0,
55
+ index: projectile.index ?? 0,
56
+ count: projectile.count ?? 1
57
+ },
58
+ component,
59
+ createdAt: now
60
+ };
61
+ this.setPredictedImpact(runtime);
62
+ this.projectiles.set(projectile.id, runtime);
63
+ this.hooks.callHooks("client-projectiles-onSpawn", runtime.spawn).subscribe();
64
+ }
65
+ this.touch();
66
+ }
67
+ impactBatch(impacts, context = {}) {
68
+ if (!this.acceptsMap(context.mapId)) return;
69
+ const now = Date.now();
70
+ for (const impact of impacts) {
71
+ const projectile = this.projectiles.get(impact.id);
72
+ if (!projectile) continue;
73
+ this.setImpact(projectile, impact, now);
74
+ this.hooks.callHooks("client-projectiles-onImpact", this.toProps(projectile, now)).subscribe();
75
+ }
76
+ this.touch();
77
+ }
78
+ destroyBatch(projectiles, context = {}) {
79
+ if (!this.acceptsMap(context.mapId)) return;
80
+ const now = Date.now();
81
+ for (const destroyed of projectiles) {
82
+ const projectile = this.projectiles.get(destroyed.id);
83
+ if (!projectile) continue;
84
+ if (destroyed.reason === "hit") {
85
+ const current = this.toProps(projectile, now);
86
+ this.setImpact(projectile, {
87
+ id: destroyed.id,
88
+ targetId: destroyed.targetId ?? projectile.impact?.targetId,
89
+ x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,
90
+ y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,
91
+ distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance
92
+ }, now);
93
+ }
94
+ projectile.destroyReason = destroyed.reason;
95
+ projectile.destroyAt = projectile.destroyAt ?? (projectile.impact && projectile.impactStartedAt !== void 0 ? projectile.impactStartedAt + this.impactDurationMs : now);
96
+ this.hooks.callHooks("client-projectiles-onDestroy", this.toProps(projectile, now)).subscribe();
97
+ }
98
+ this.touch();
99
+ }
100
+ clear() {
101
+ this.projectiles.clear();
102
+ this.touch();
103
+ }
104
+ step() {
105
+ const now = Date.now();
106
+ let changed = false;
107
+ for (const [id, projectile] of this.projectiles) if (!this.toProps(projectile, now) && !this.isWaitingForDelay(projectile, now) || projectile.destroyAt !== void 0 && now >= projectile.destroyAt) {
108
+ this.projectiles.delete(id);
109
+ changed = true;
110
+ }
111
+ this.touch(changed || this.projectiles.size > 0);
112
+ }
113
+ toProps(projectile, now) {
114
+ const spawn = projectile.spawn;
115
+ const delayMs = (spawn.delay ?? 0) * 1e3;
116
+ const elapsedMs = now - projectile.createdAt - delayMs;
117
+ if (elapsedMs < 0) return null;
118
+ const elapsed = elapsedMs / 1e3;
119
+ const ttl = Math.max(.001, spawn.ttl);
120
+ const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);
121
+ const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);
122
+ const visualImpact = projectile.visualImpact ?? projectile.impact;
123
+ const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;
124
+ const progress = Math.min(1, distance / spawn.range);
125
+ const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;
126
+ const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;
127
+ const impactElapsedMs = projectile.impactStartedAt !== void 0 ? Math.max(0, now - projectile.impactStartedAt) : void 0;
128
+ return {
129
+ ...spawn,
130
+ x,
131
+ y,
132
+ angle: Math.atan2(spawn.direction.y, spawn.direction.x),
133
+ distance,
134
+ elapsed,
135
+ progress,
136
+ impact: projectile.impact,
137
+ impactElapsed: impactElapsedMs === void 0 ? void 0 : impactElapsedMs / 1e3,
138
+ impactProgress: impactElapsedMs === void 0 ? void 0 : Math.min(1, impactElapsedMs / this.impactDurationMs),
139
+ destroyed: projectile.destroyAt !== void 0,
140
+ ttl
141
+ };
142
+ }
143
+ acceptsMap(mapId) {
144
+ const normalizedMapId = normalizeRoomMapId(mapId);
145
+ return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
146
+ }
147
+ isWaitingForDelay(projectile, now) {
148
+ const delayMs = (projectile.spawn.delay ?? 0) * 1e3;
149
+ return now - projectile.createdAt - delayMs < 0;
150
+ }
151
+ setPredictedImpact(projectile) {
152
+ if (projectile.spawn.predictImpact === false) return;
153
+ const impact = this.predictionResolver?.(projectile.spawn);
154
+ if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) return;
155
+ const distance = typeof impact.distance === "number" && Number.isFinite(impact.distance) ? impact.distance : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);
156
+ if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) return;
157
+ projectile.predictedImpact = {
158
+ ...impact,
159
+ distance
160
+ };
161
+ }
162
+ getActivePredictedImpact(projectile, now, rawDistance) {
163
+ if (!projectile.predictedImpact || projectile.impact) return;
164
+ const distance = projectile.predictedImpact.distance;
165
+ if (distance === void 0 || rawDistance < distance) return;
166
+ return projectile.predictedImpact;
167
+ }
168
+ setImpact(projectile, impact, now) {
169
+ projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);
170
+ projectile.impact = impact;
171
+ projectile.predictedImpact = void 0;
172
+ projectile.impactStartedAt = projectile.impactStartedAt ?? now;
173
+ const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;
174
+ projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);
175
+ }
176
+ resolveVisualImpact(projectile, impact, now) {
177
+ const predicted = projectile.predictedImpact;
178
+ if (!predicted || !this.isSameTarget(predicted, impact)) return impact;
179
+ const distance = predicted.distance;
180
+ if (distance === void 0) return impact;
181
+ const delayMs = (projectile.spawn.delay ?? 0) * 1e3;
182
+ const elapsedMs = now - projectile.createdAt - delayMs;
183
+ if (elapsedMs < 0) return impact;
184
+ return Math.min(projectile.spawn.speed * (elapsedMs / 1e3), projectile.spawn.range) >= distance ? predicted : impact;
185
+ }
186
+ isSameTarget(a, b) {
187
+ return a.targetId !== void 0 && a.targetId === b.targetId;
188
+ }
189
+ touch(force = true) {
190
+ if (force) this.version.update((value) => value + 1);
191
+ }
192
+ };
193
+ //#endregion
194
+ export { ProjectileManager };
195
+
196
+ //# sourceMappingURL=ProjectileManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectileManager.js","names":[],"sources":["../../src/Game/ProjectileManager.ts"],"sourcesContent":["import { computed, signal } from \"canvasengine\";\nimport { Hooks } from \"@rpgjs/common\";\nimport { normalizeRoomMapId } from \"../utils/mapId\";\n\nexport interface ClientProjectileSpawn {\n id: string;\n type: string;\n ownerId?: string;\n origin: { x: number; y: number };\n direction: { x: number; y: number };\n speed: number;\n range: number;\n ttl: number;\n spawnTick: number;\n delay?: number;\n index?: number;\n count?: number;\n params?: Record<string, unknown>;\n collisionMask?: number;\n ignoreOwner?: boolean;\n predictImpact?: boolean;\n}\n\nexport interface ClientProjectileImpact {\n id: string;\n targetId?: string;\n x: number;\n y: number;\n distance?: number;\n}\n\nexport interface ClientProjectileDestroy {\n id: string;\n reason?: string;\n targetId?: string;\n x?: number;\n y?: number;\n distance?: number;\n}\n\nexport interface RenderedProjectileProps extends ClientProjectileSpawn {\n x: number;\n y: number;\n angle: number;\n distance: number;\n elapsed: number;\n progress: number;\n impact?: ClientProjectileImpact;\n impactElapsed?: number;\n impactProgress?: number;\n destroyed?: boolean;\n}\n\nexport interface RenderedProjectile {\n id: string;\n type: string;\n component: any;\n props: RenderedProjectileProps;\n}\n\nexport type ProjectilePredictionResolver = (\n projectile: ClientProjectileSpawn,\n) => ClientProjectileImpact | null | undefined;\n\nexport interface ProjectileSpawnClock {\n now?: number;\n currentServerTick?: number;\n tickDurationMs?: number;\n mapId?: string;\n}\n\ninterface RuntimeProjectile {\n spawn: ClientProjectileSpawn;\n component: any;\n createdAt: number;\n impact?: ClientProjectileImpact;\n visualImpact?: ClientProjectileImpact;\n predictedImpact?: ClientProjectileImpact;\n impactStartedAt?: number;\n destroyAt?: number;\n destroyReason?: string;\n}\n\nexport class ProjectileManager {\n private readonly components = new Map<string, any>();\n private readonly projectiles = new Map<string, RuntimeProjectile>();\n private readonly version = signal(0);\n private readonly impactDurationMs = 350;\n private mapId?: string;\n\n constructor(\n private readonly hooks: Hooks,\n private readonly predictionResolver?: ProjectilePredictionResolver,\n ) {}\n\n current = computed<RenderedProjectile[]>(() => {\n this.version();\n const now = Date.now();\n const rendered: RenderedProjectile[] = [];\n for (const projectile of this.projectiles.values()) {\n const props = this.toProps(projectile, now);\n if (!props) {\n continue;\n }\n rendered.push({\n id: projectile.spawn.id,\n type: projectile.spawn.type,\n component: projectile.component,\n props,\n });\n }\n return rendered;\n });\n\n register(type: string, component: any): any {\n this.components.set(type, component);\n return component;\n }\n\n get(type: string): any {\n return this.components.get(type);\n }\n\n setMapId(mapId: string | undefined): void {\n const normalizedMapId = normalizeRoomMapId(mapId);\n if (this.mapId === normalizedMapId) return;\n this.mapId = normalizedMapId;\n this.clear();\n }\n\n getMapId(): string | undefined {\n return this.mapId;\n }\n\n spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {\n if (!this.acceptsMap(clock.mapId)) return;\n const now = clock.now ?? Date.now();\n for (const projectile of projectiles) {\n const component = this.components.get(projectile.type);\n if (!component) {\n continue;\n }\n const runtime: RuntimeProjectile = {\n spawn: {\n ...projectile,\n delay: projectile.delay ?? 0,\n index: projectile.index ?? 0,\n count: projectile.count ?? 1,\n },\n component,\n createdAt: now,\n };\n this.setPredictedImpact(runtime);\n this.projectiles.set(projectile.id, runtime);\n this.hooks.callHooks(\"client-projectiles-onSpawn\", runtime.spawn).subscribe();\n }\n this.touch();\n }\n\n impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {\n if (!this.acceptsMap(context.mapId)) return;\n const now = Date.now();\n for (const impact of impacts) {\n const projectile = this.projectiles.get(impact.id);\n if (!projectile) {\n continue;\n }\n this.setImpact(projectile, impact, now);\n this.hooks.callHooks(\"client-projectiles-onImpact\", this.toProps(projectile, now)).subscribe();\n }\n this.touch();\n }\n\n destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {\n if (!this.acceptsMap(context.mapId)) return;\n const now = Date.now();\n for (const destroyed of projectiles) {\n const projectile = this.projectiles.get(destroyed.id);\n if (!projectile) {\n continue;\n }\n if (destroyed.reason === \"hit\") {\n const current = this.toProps(projectile, now);\n this.setImpact(projectile, {\n id: destroyed.id,\n targetId: destroyed.targetId ?? projectile.impact?.targetId,\n x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,\n y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,\n distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance,\n }, now);\n }\n projectile.destroyReason = destroyed.reason;\n projectile.destroyAt = projectile.destroyAt ?? (\n projectile.impact && projectile.impactStartedAt !== undefined\n ? projectile.impactStartedAt + this.impactDurationMs\n : now\n );\n this.hooks.callHooks(\"client-projectiles-onDestroy\", this.toProps(projectile, now)).subscribe();\n }\n this.touch();\n }\n\n clear(): void {\n this.projectiles.clear();\n this.touch();\n }\n\n step(): void {\n const now = Date.now();\n let changed = false;\n for (const [id, projectile] of this.projectiles) {\n const props = this.toProps(projectile, now);\n if (\n (!props && !this.isWaitingForDelay(projectile, now)) ||\n (projectile.destroyAt !== undefined && now >= projectile.destroyAt)\n ) {\n this.projectiles.delete(id);\n changed = true;\n }\n }\n this.touch(changed || this.projectiles.size > 0);\n }\n\n private toProps(projectile: RuntimeProjectile, now: number): RenderedProjectileProps | null {\n const spawn = projectile.spawn;\n const delayMs = (spawn.delay ?? 0) * 1000;\n const elapsedMs = now - projectile.createdAt - delayMs;\n if (elapsedMs < 0) {\n return null;\n }\n const elapsed = elapsedMs / 1000;\n const ttl = Math.max(0.001, spawn.ttl);\n const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);\n const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);\n const visualImpact = projectile.visualImpact ?? projectile.impact;\n const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;\n const progress = Math.min(1, distance / spawn.range);\n const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;\n const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;\n const impactElapsedMs = projectile.impactStartedAt !== undefined\n ? Math.max(0, now - projectile.impactStartedAt)\n : undefined;\n return {\n ...spawn,\n x,\n y,\n angle: Math.atan2(spawn.direction.y, spawn.direction.x),\n distance,\n elapsed,\n progress,\n impact: projectile.impact,\n impactElapsed: impactElapsedMs === undefined ? undefined : impactElapsedMs / 1000,\n impactProgress: impactElapsedMs === undefined\n ? undefined\n : Math.min(1, impactElapsedMs / this.impactDurationMs),\n destroyed: projectile.destroyAt !== undefined,\n ttl,\n };\n }\n\n private acceptsMap(mapId: string | undefined): boolean {\n const normalizedMapId = normalizeRoomMapId(mapId);\n return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;\n }\n\n private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {\n const delayMs = (projectile.spawn.delay ?? 0) * 1000;\n return now - projectile.createdAt - delayMs < 0;\n }\n\n private setPredictedImpact(projectile: RuntimeProjectile): void {\n if (projectile.spawn.predictImpact === false) {\n return;\n }\n const impact = this.predictionResolver?.(projectile.spawn);\n if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) {\n return;\n }\n const distance = typeof impact.distance === \"number\" && Number.isFinite(impact.distance)\n ? impact.distance\n : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);\n if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) {\n return;\n }\n projectile.predictedImpact = {\n ...impact,\n distance,\n };\n }\n\n private getActivePredictedImpact(\n projectile: RuntimeProjectile,\n now: number,\n rawDistance: number,\n ): ClientProjectileImpact | undefined {\n if (!projectile.predictedImpact || projectile.impact) {\n return undefined;\n }\n const distance = projectile.predictedImpact.distance;\n if (distance === undefined || rawDistance < distance) {\n return undefined;\n }\n return projectile.predictedImpact;\n }\n\n private setImpact(projectile: RuntimeProjectile, impact: ClientProjectileImpact, now: number): void {\n projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);\n projectile.impact = impact;\n projectile.predictedImpact = undefined;\n projectile.impactStartedAt = projectile.impactStartedAt ?? now;\n const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;\n projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);\n }\n\n private resolveVisualImpact(\n projectile: RuntimeProjectile,\n impact: ClientProjectileImpact,\n now: number,\n ): ClientProjectileImpact {\n const predicted = projectile.predictedImpact;\n if (!predicted || !this.isSameTarget(predicted, impact)) {\n return impact;\n }\n const distance = predicted.distance;\n if (distance === undefined) {\n return impact;\n }\n const delayMs = (projectile.spawn.delay ?? 0) * 1000;\n const elapsedMs = now - projectile.createdAt - delayMs;\n if (elapsedMs < 0) {\n return impact;\n }\n const rawDistance = Math.min(projectile.spawn.speed * (elapsedMs / 1000), projectile.spawn.range);\n return rawDistance >= distance ? predicted : impact;\n }\n\n private isSameTarget(a: ClientProjectileImpact, b: ClientProjectileImpact): boolean {\n return a.targetId !== undefined && a.targetId === b.targetId;\n }\n\n private touch(force = true): void {\n if (force) {\n this.version.update((value) => value + 1);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -2,7 +2,10 @@ 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 { MapPhysicsEntityContext, MapPhysicsInitContext } from '@rpgjs/common';
5
+ import { RpgClientEvent } from './Game/Event';
6
+ import { MapPhysicsEntityContext, MapPhysicsInitContext, RpgActionName } from '@rpgjs/common';
7
+ import { ClientProjectileSpawn, RenderedProjectileProps } from './Game/ProjectileManager';
8
+ import { ClientVisualMap } from './Game/ClientVisuals';
6
9
  type RpgComponent = RpgClientObject;
7
10
  type SceneMap = Container;
8
11
  export type SpriteComponentConfig = ComponentFunction | {
@@ -11,6 +14,14 @@ export type SpriteComponentConfig = ComponentFunction | {
11
14
  data?: Record<string, any> | ((object: RpgClientObject) => Record<string, any>);
12
15
  dependencies?: (object: RpgClientObject) => any[];
13
16
  };
17
+ export type EventComponentSprite = RpgClientEvent & Record<string, any>;
18
+ export type EventComponentConfig = ComponentFunction | {
19
+ component: ComponentFunction;
20
+ props?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>);
21
+ data?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>);
22
+ dependencies?: (event: EventComponentSprite) => any[];
23
+ renderGraphic?: boolean;
24
+ };
14
25
  export interface RpgSpriteBeforeRemoveContext {
15
26
  reason?: string;
16
27
  data?: any;
@@ -40,15 +51,18 @@ export interface RpgClientEngineHooks {
40
51
  /**
41
52
  * Recover keys from the pressed keyboard
42
53
  *
43
- * @prop { (engine: RpgClientEngine, obj: { input: string, playerId: number }) => any } [onInput]
54
+ * @prop { (engine: RpgClientEngine, obj: { input: string | number, action?: string | number, data?: any, playerId: number }) => any } [onInput]
44
55
  * @memberof RpgEngineHooks
45
56
  */
46
57
  onInput?: (engine: RpgClientEngine, obj: {
47
- input: string;
58
+ input: RpgActionName;
59
+ action?: RpgActionName;
60
+ data?: any;
48
61
  playerId: number;
49
62
  }) => any;
50
63
  /**
51
- * Called when the user is connected to the server
64
+ * Called when the user is connected to the server. In MMORPG mode, this
65
+ * runs after the server sends the RPGJS connection acceptance packet.
52
66
  *
53
67
  * @prop { (engine: RpgClientEngine, socket: any) => any } [onConnected]
54
68
  * @memberof RpgEngineHooks
@@ -62,7 +76,8 @@ export interface RpgClientEngineHooks {
62
76
  */
63
77
  onDisconnect?: (engine: RpgClientEngine, reason: any, socket: any) => any;
64
78
  /**
65
- * Called when there was a connection error
79
+ * Called when there was a connection error. In MMORPG mode, this also runs
80
+ * when server-side auth refuses the connection.
66
81
  *
67
82
  * @prop { (engine: RpgClientEngine, err: any, socket: any) => any } [onConnectError]
68
83
  * @memberof RpgEngineHooks
@@ -126,6 +141,30 @@ export interface RpgSpriteHooks {
126
141
  * ```
127
142
  */
128
143
  components?: Record<string, ComponentFunction>;
144
+ /**
145
+ * Resolve a custom CanvasEngine component for a specific event.
146
+ *
147
+ * The component always receives the synced event object as the `sprite` prop.
148
+ * Custom props are merged in addition to `sprite`, but cannot replace it.
149
+ * Return `null` or `undefined` to keep the default graphic renderer.
150
+ *
151
+ * @prop { (event: EventComponentSprite) => EventComponentConfig | null | undefined } [eventComponent]
152
+ * @memberof RpgSpriteHooks
153
+ * @example
154
+ * ```ts
155
+ * import ChestEvent from './components/chest-event.ce'
156
+ *
157
+ * const sprite: RpgSpriteHooks = {
158
+ * eventComponent(sprite) {
159
+ * if (sprite.name === 'CHEST') {
160
+ * return ChestEvent
161
+ * }
162
+ * return null
163
+ * }
164
+ * }
165
+ * ```
166
+ */
167
+ eventComponent?: (event: EventComponentSprite) => EventComponentConfig | null | undefined;
129
168
  /**
130
169
  * As soon as the sprite is initialized
131
170
  *
@@ -222,6 +261,13 @@ export interface RpgSceneHooks<Scene> {
222
261
  onDraw?: (scene: Scene, t: number) => any;
223
262
  }
224
263
  export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
264
+ /**
265
+ * Root CanvasEngine component used to render the RPG scene map.
266
+ *
267
+ * Use the exported `SceneMap` component inside your custom component to
268
+ * keep the default map rendering and compose additional scene children.
269
+ */
270
+ component?: ComponentFunction;
225
271
  /**
226
272
  * The map and resources are being loaded
227
273
  *
@@ -261,6 +307,24 @@ export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
261
307
  */
262
308
  onPhysicsReset?: (scene: SceneMap) => any;
263
309
  }
310
+ export interface RpgProjectileHooks {
311
+ /**
312
+ * CanvasEngine components used to render server-authoritative projectiles.
313
+ */
314
+ components?: Record<string, ComponentFunction>;
315
+ /**
316
+ * Called when a projectile spawn batch is received from the server.
317
+ */
318
+ onSpawn?: (projectile: ClientProjectileSpawn) => any;
319
+ /**
320
+ * Called when the server confirms a projectile impact.
321
+ */
322
+ onImpact?: (projectile: RenderedProjectileProps | null) => any;
323
+ /**
324
+ * Called when the server destroys a projectile.
325
+ */
326
+ onDestroy?: (projectile: RenderedProjectileProps | null) => any;
327
+ }
264
328
  export interface RpgClient {
265
329
  /**
266
330
  * Add hooks to the player or engine. All modules can listen to the hook
@@ -588,30 +652,33 @@ export interface RpgClient {
588
652
  * */
589
653
  sprite?: RpgSpriteHooks;
590
654
  /**
591
- * Reference the scenes of the game. Here you can put your own class that inherits RpgSceneMap
655
+ * Reference the scenes of the game.
592
656
  *
593
657
  * ```ts
594
658
  * import { RpgSceneMapHooks, RpgClient, defineModule } from '@rpgjs/client'
659
+ * import MyScene from './my-scene.ce'
595
660
  *
596
661
  * export const sceneMap: RpgSceneMapHooks = {
597
- *
662
+ * component: MyScene
598
663
  * }
599
664
  *
600
665
  * defineModule<RpgClient>({
601
- * scenes: {
602
- * // If you put the RpgSceneMap scene, Thhe key is called mandatory `map`
603
- * map: sceneMap
604
- * }
666
+ * sceneMap
605
667
  * })
606
668
  * ```
607
669
  *
608
- * @prop { [sceneName: string]: RpgSceneMapHooks } [scenes]
670
+ * @prop {RpgSceneMapHooks} [sceneMap]
609
671
  * @memberof RpgClient
610
672
  * */
673
+ sceneMap?: RpgSceneMapHooks;
674
+ /**
675
+ * Legacy scene map hook container.
676
+ *
677
+ * Prefer `sceneMap` for new code.
678
+ */
611
679
  scenes?: {
612
680
  map: RpgSceneMapHooks;
613
681
  };
614
- sceneMap?: RpgSceneMapHooks;
615
682
  /**
616
683
  * Array containing the list of component animations
617
684
  * Each element defines a temporary component to display for animations like hits, effects, etc.
@@ -642,5 +709,42 @@ export interface RpgClient {
642
709
  id: string;
643
710
  component: ComponentFunction;
644
711
  }[];
712
+ /**
713
+ * Named client-side visual macros.
714
+ *
715
+ * Use client visuals when the server needs to trigger a group of existing
716
+ * client visual primitives at once, such as a flash, damage text, sound,
717
+ * component animation, and camera shake. The server sends only the visual
718
+ * name and a serializable payload; the rendering details live on the client.
719
+ *
720
+ * For a single sound, flash, or component animation, prefer the direct
721
+ * server APIs (`playSound`, `flash`, `showComponentAnimation`). Client
722
+ * visuals are meant to group several visual operations and reduce bandwidth.
723
+ *
724
+ * ```ts
725
+ * import { defineModule, RpgClient } from '@rpgjs/client'
726
+ *
727
+ * export default defineModule<RpgClient>({
728
+ * clientVisuals: {
729
+ * hit({ target, data }, helpers) {
730
+ * helpers.flash(target, { type: 'tint', tint: 'red' })
731
+ * helpers.showHit(target, `-${data.damage}`)
732
+ * helpers.sound('hit')
733
+ * }
734
+ * }
735
+ * })
736
+ * ```
737
+ *
738
+ * @prop {Record<string, ClientVisualHandler>} [clientVisuals]
739
+ * @memberof RpgClient
740
+ */
741
+ clientVisuals?: ClientVisualMap;
742
+ /**
743
+ * Client-side projectile rendering configuration.
744
+ *
745
+ * Register a CanvasEngine component per projectile type. The server sends
746
+ * compact spawn/impact/destroy events and the client predicts x/y locally.
747
+ */
748
+ projectiles?: RpgProjectileHooks;
645
749
  }
646
750
  export {};
@@ -1,9 +1,15 @@
1
1
  import { Trigger } from 'canvasengine';
2
2
  import { AbstractWebsocket } from './services/AbstractSocket';
3
- import { Direction } from '@rpgjs/common';
3
+ import { Direction, RpgActionInput, RpgActionName } from '@rpgjs/common';
4
+ import { EventComponentConfig } from './RpgClient';
5
+ import { RpgClientEvent } from './Game/Event';
4
6
  import { RpgClientMap } from './Game/Map';
5
7
  import { AnimationManager } from './Game/AnimationManager';
6
8
  import { Observable } from 'rxjs';
9
+ import { ProjectileManager } from './Game/ProjectileManager';
10
+ import { ClientVisualRegistry, ClientVisualHandler, ClientVisualMap, ClientVisualPacket } from './Game/ClientVisuals';
11
+ import { ClientPointerContext } from './services/pointerContext';
12
+ import { EventComponentResolver } from './Game/EventComponentResolver';
7
13
  import * as PIXI from "pixi.js";
8
14
  type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
9
15
  start(config?: T): Promise<void>;
@@ -24,12 +30,16 @@ export declare class RpgClientEngine<T = any> {
24
30
  private selector;
25
31
  globalConfig: T;
26
32
  sceneComponent: any;
33
+ sceneMapComponent: any;
27
34
  stopProcessingInput: boolean;
28
35
  width: import('canvasengine').WritableSignal<string>;
29
36
  height: import('canvasengine').WritableSignal<string>;
30
37
  spritesheets: Map<string | number, any>;
31
38
  sounds: Map<string, any>;
32
39
  componentAnimations: any[];
40
+ clientVisuals: ClientVisualRegistry;
41
+ projectiles: ProjectileManager;
42
+ pointer: ClientPointerContext;
33
43
  private spritesheetResolver?;
34
44
  private soundResolver?;
35
45
  particleSettings: {
@@ -43,6 +53,7 @@ export declare class RpgClientEngine<T = any> {
43
53
  spriteComponentsBehind: import('canvasengine').WritableArraySignal<any[]>;
44
54
  spriteComponentsInFront: import('canvasengine').WritableArraySignal<any[]>;
45
55
  spriteComponents: Map<string, any>;
56
+ private eventComponentResolvers;
46
57
  /** ID of the sprite that the camera should follow. null means follow the current player */
47
58
  cameraFollowTargetId: import('canvasengine').WritableSignal<string | null>;
48
59
  /** Trigger for map shake animation */
@@ -56,6 +67,8 @@ export declare class RpgClientEngine<T = any> {
56
67
  private pendingPredictionFrames;
57
68
  private lastClientPhysicsStepAt;
58
69
  private frameOffset;
70
+ private latestServerTick?;
71
+ private latestServerTickAt;
59
72
  private rtt;
60
73
  private pingInterval;
61
74
  private readonly PING_INTERVAL_MS;
@@ -70,8 +83,14 @@ export declare class RpgClientEngine<T = any> {
70
83
  private eventsReceived$;
71
84
  private onAfterLoadingSubscription?;
72
85
  private sceneResetQueued;
86
+ private mapTransitionInProgress;
87
+ private currentMapRoomId?;
88
+ private socketListenersInitialized;
73
89
  private tickSubscriptions;
74
90
  private resizeHandler?;
91
+ private pointerMoveHandler?;
92
+ private pointerCanvas?;
93
+ private pendingSyncPackets;
75
94
  private notificationManager;
76
95
  constructor(context: any);
77
96
  /**
@@ -109,9 +128,18 @@ export declare class RpgClientEngine<T = any> {
109
128
  */
110
129
  setKeyboardControls(controlInstance: any): void;
111
130
  start(): Promise<void>;
131
+ private resolveSceneMapComponent;
132
+ private setupPointerTracking;
133
+ private findViewportInstance;
112
134
  private prepareSyncPayload;
113
135
  private normalizeAckWithSyncState;
114
136
  private initListeners;
137
+ private beginMapTransfer;
138
+ private clearComponentAnimations;
139
+ private shouldProcessProjectilePacket;
140
+ private callConnectError;
141
+ private flushPendingSyncPackets;
142
+ private applySyncPacket;
115
143
  /**
116
144
  * Start periodic ping/pong for client-server synchronization
117
145
  *
@@ -486,6 +514,52 @@ export declare class RpgClientEngine<T = any> {
486
514
  * @returns The CanvasEngine component, or undefined when missing
487
515
  */
488
516
  getSpriteComponent(id: string): any;
517
+ /**
518
+ * Register a custom event component resolver.
519
+ *
520
+ * The last resolver returning a component wins. This lets later modules
521
+ * override earlier defaults without replacing the whole map scene.
522
+ *
523
+ * @param resolver - Function receiving the synced event object
524
+ * @returns The registered resolver
525
+ */
526
+ addEventComponentResolver(resolver: EventComponentResolver): EventComponentResolver;
527
+ /**
528
+ * Resolve the custom CanvasEngine component for an event, if any.
529
+ *
530
+ * @param event - Synced client event object
531
+ * @returns The component/config returned by the last matching resolver
532
+ */
533
+ resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null;
534
+ registerProjectileComponent(type: string, component: any): any;
535
+ getProjectileComponent(type: string): any;
536
+ /**
537
+ * Register a named client visual macro.
538
+ *
539
+ * Client visuals are small client-side functions that group existing visual
540
+ * primitives such as flash, sound, component animations, sprite animation, or
541
+ * map shake. The server sends only the visual name and a serializable payload.
542
+ *
543
+ * @param name - Stable visual name sent by the server
544
+ * @param handler - Client-side visual handler
545
+ * @returns The registered handler
546
+ */
547
+ registerClientVisual(name: string, handler: ClientVisualHandler): ClientVisualHandler;
548
+ /**
549
+ * Register several named client visual macros.
550
+ *
551
+ * @param visuals - Map of visual names to client-side handlers
552
+ */
553
+ registerClientVisuals(visuals: ClientVisualMap): void;
554
+ /**
555
+ * Play a registered client visual locally.
556
+ *
557
+ * This is also used by the websocket listener when the server calls
558
+ * `player.clientVisual()` or `map.clientVisual()`.
559
+ *
560
+ * @param packet - Visual name and serializable payload
561
+ */
562
+ playClientVisual(packet: ClientVisualPacket): Promise<void>;
489
563
  /**
490
564
  * Add a component animation to the engine
491
565
  *
@@ -564,14 +638,18 @@ export declare class RpgClientEngine<T = any> {
564
638
  processInput({ input }: {
565
639
  input: Direction;
566
640
  }): Promise<void>;
567
- processAction({ action }: {
568
- action: number;
569
- }): void;
641
+ processAction(action: RpgActionName, data?: any): void;
642
+ processAction(action: RpgActionInput): void;
570
643
  get PIXI(): typeof PIXI;
571
644
  get socket(): AbstractWebsocket;
572
645
  get playerId(): string | null;
573
646
  get scene(): RpgClientMap;
647
+ getObjectById(id: string): any;
574
648
  private getPhysicsTick;
649
+ private getPhysicsTickDurationMs;
650
+ private updateServerTickEstimate;
651
+ private estimateServerTick;
652
+ private predictProjectileImpact;
575
653
  private ensureCurrentPlayerBody;
576
654
  private stepClientPhysicsTick;
577
655
  private flushPendingPredictedStates;