@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.
- package/CHANGELOG.md +21 -0
- package/dist/Game/AnimationManager.d.ts +1 -0
- package/dist/Game/AnimationManager.js +3 -0
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/ClientVisuals.d.ts +61 -0
- package/dist/Game/ClientVisuals.js +96 -0
- package/dist/Game/ClientVisuals.js.map +1 -0
- package/dist/Game/ClientVisuals.spec.d.ts +1 -0
- package/dist/Game/EventComponentResolver.d.ts +16 -0
- package/dist/Game/EventComponentResolver.js +52 -0
- package/dist/Game/EventComponentResolver.js.map +1 -0
- package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
- package/dist/Game/Map.js +9 -0
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +2 -2
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/ProjectileManager.d.ts +98 -0
- package/dist/Game/ProjectileManager.js +196 -0
- package/dist/Game/ProjectileManager.js.map +1 -0
- package/dist/Game/ProjectileManager.spec.d.ts +1 -0
- package/dist/RpgClient.d.ts +117 -13
- package/dist/RpgClientEngine.d.ts +82 -4
- package/dist/RpgClientEngine.js +296 -51
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/animations/fx.ce.js +58 -0
- package/dist/components/animations/fx.ce.js.map +1 -0
- package/dist/components/animations/hit.ce.js.map +1 -1
- package/dist/components/animations/index.d.ts +1 -0
- package/dist/components/animations/index.js +3 -1
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +140 -40
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/dynamics/bar.ce.js +4 -3
- package/dist/components/dynamics/bar.ce.js.map +1 -1
- package/dist/components/dynamics/image.ce.js +2 -1
- package/dist/components/dynamics/image.ce.js.map +1 -1
- package/dist/components/dynamics/shape.ce.js +3 -2
- package/dist/components/dynamics/shape.ce.js.map +1 -1
- package/dist/components/dynamics/text.ce.js +9 -8
- package/dist/components/dynamics/text.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +3 -2
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +3 -2
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/hud/hud.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +2 -1
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +2 -1
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +3 -2
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +3 -2
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +2 -1
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +3 -2
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +3 -2
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -0
- package/dist/components/player-components.ce.js +11 -10
- package/dist/components/player-components.ce.js.map +1 -1
- package/dist/components/prebuilt/hp-bar.ce.js +4 -3
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
- package/dist/components/prebuilt/light-halo.ce.js +2 -1
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +12 -4
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +6 -3
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -5
- package/dist/module.js +18 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +14 -0
- package/dist/services/actionInput.js +59 -0
- package/dist/services/actionInput.js.map +1 -0
- package/dist/services/actionInput.spec.d.ts +1 -0
- package/dist/services/mmorpg-connection.d.ts +5 -0
- package/dist/services/mmorpg-connection.js +50 -0
- package/dist/services/mmorpg-connection.js.map +1 -0
- package/dist/services/mmorpg-connection.spec.d.ts +1 -0
- package/dist/services/mmorpg.d.ts +10 -4
- package/dist/services/mmorpg.js +48 -30
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/pointerContext.d.ts +11 -0
- package/dist/services/pointerContext.js +48 -0
- package/dist/services/pointerContext.js.map +1 -0
- package/dist/services/pointerContext.spec.d.ts +1 -0
- package/dist/services/standalone-message.d.ts +1 -0
- package/dist/services/standalone-message.js +9 -0
- package/dist/services/standalone-message.js.map +1 -0
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +34 -15
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- package/dist/utils/mapId.d.ts +1 -0
- package/dist/utils/mapId.js +6 -0
- package/dist/utils/mapId.js.map +1 -0
- package/package.json +7 -7
- package/src/Game/AnimationManager.ts +4 -0
- package/src/Game/ClientVisuals.spec.ts +56 -0
- package/src/Game/ClientVisuals.ts +184 -0
- package/src/Game/EventComponentResolver.spec.ts +84 -0
- package/src/Game/EventComponentResolver.ts +74 -0
- package/src/Game/Map.ts +10 -0
- package/src/Game/Object.spec.ts +46 -0
- package/src/Game/Object.ts +2 -2
- package/src/Game/ProjectileManager.spec.ts +449 -0
- package/src/Game/ProjectileManager.ts +346 -0
- package/src/RpgClient.ts +130 -15
- package/src/RpgClientEngine.ts +405 -69
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +185 -40
- package/src/components/dynamics/bar.ce +4 -3
- package/src/components/dynamics/image.ce +2 -1
- package/src/components/dynamics/shape.ce +3 -2
- package/src/components/dynamics/text.ce +9 -8
- package/src/components/gui/dialogbox/index.ce +3 -2
- package/src/components/gui/gameover.ce +2 -1
- package/src/components/gui/menu/equip-menu.ce +2 -1
- package/src/components/gui/menu/exit-menu.ce +2 -1
- package/src/components/gui/menu/items-menu.ce +3 -2
- package/src/components/gui/menu/main-menu.ce +2 -1
- package/src/components/gui/save-load.ce +2 -1
- package/src/components/gui/shop/shop.ce +3 -2
- package/src/components/gui/title-screen.ce +2 -1
- package/src/components/index.ts +2 -1
- package/src/components/player-components.ce +11 -10
- package/src/components/prebuilt/hp-bar.ce +4 -3
- package/src/components/prebuilt/light-halo.ce +2 -2
- package/src/components/scenes/canvas.ce +10 -2
- package/src/components/scenes/draw-map.ce +17 -3
- package/src/index.ts +4 -0
- package/src/module.ts +24 -0
- package/src/services/actionInput.spec.ts +155 -0
- package/src/services/actionInput.ts +120 -0
- package/src/services/mmorpg-connection.spec.ts +99 -0
- package/src/services/mmorpg-connection.ts +69 -0
- package/src/services/mmorpg.ts +60 -34
- package/src/services/pointerContext.spec.ts +36 -0
- package/src/services/pointerContext.ts +84 -0
- package/src/services/standalone-message.ts +7 -0
- package/src/services/standalone.spec.ts +34 -0
- package/src/services/standalone.ts +42 -12
- 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
|
|
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:
|
|
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.
|
|
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
|
-
*
|
|
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 {
|
|
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
|
}
|