@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
package/src/RpgClientEngine.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import Canvas from "./components/scenes/canvas.ce";
|
|
2
|
+
import BuiltinSceneMap from "./components/scenes/draw-map.ce";
|
|
2
3
|
import { inject } from './core/inject'
|
|
3
4
|
import { signal, bootstrapCanvas, Howl, trigger, type Trigger } from "canvasengine";
|
|
4
5
|
import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
|
|
5
6
|
import { LoadMapService, LoadMapToken } from "./services/loadMap";
|
|
6
7
|
import { RpgSound } from "./Sound";
|
|
7
8
|
import { RpgResource } from "./Resource";
|
|
8
|
-
import { Hooks, ModulesToken, Direction, normalizeLightingState } from "@rpgjs/common";
|
|
9
|
+
import { Hooks, ModulesToken, Direction, normalizeLightingState, Vector2 } from "@rpgjs/common";
|
|
10
|
+
import type { EventComponentConfig } from "./RpgClient";
|
|
11
|
+
import type { RpgClientEvent } from "./Game/Event";
|
|
9
12
|
import { load } from "@signe/sync";
|
|
10
13
|
import { RpgClientMap } from "./Game/Map"
|
|
11
14
|
import { RpgGui } from "./Gui/Gui";
|
|
@@ -22,10 +25,18 @@ import {
|
|
|
22
25
|
PredictionController,
|
|
23
26
|
type PredictionHistoryEntry,
|
|
24
27
|
type PredictionState,
|
|
28
|
+
type RpgActionInput,
|
|
29
|
+
type RpgActionName,
|
|
25
30
|
} from "@rpgjs/common";
|
|
26
31
|
import { NotificationManager } from "./Gui/NotificationManager";
|
|
27
32
|
import { SaveClientService } from "./services/save";
|
|
28
33
|
import { getCanMoveValue } from "./utils/readPropValue";
|
|
34
|
+
import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
|
|
35
|
+
import { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from "./Game/ClientVisuals";
|
|
36
|
+
import { normalizeActionInput } from "./services/actionInput";
|
|
37
|
+
import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
|
|
38
|
+
import { normalizeRoomMapId } from "./utils/mapId";
|
|
39
|
+
import { EventComponentResolverRegistry, type EventComponentResolver } from "./Game/EventComponentResolver";
|
|
29
40
|
|
|
30
41
|
interface MovementTrajectoryPoint {
|
|
31
42
|
frame: number;
|
|
@@ -57,12 +68,16 @@ export class RpgClientEngine<T = any> {
|
|
|
57
68
|
private selector: HTMLElement;
|
|
58
69
|
public globalConfig: T;
|
|
59
70
|
public sceneComponent: any;
|
|
71
|
+
public sceneMapComponent: any = BuiltinSceneMap;
|
|
60
72
|
stopProcessingInput = false;
|
|
61
73
|
width = signal("100%");
|
|
62
74
|
height = signal("100%");
|
|
63
75
|
spritesheets: Map<string | number, any> = new Map();
|
|
64
76
|
sounds: Map<string, any> = new Map();
|
|
65
77
|
componentAnimations: any[] = [];
|
|
78
|
+
clientVisuals = new ClientVisualRegistry();
|
|
79
|
+
projectiles: ProjectileManager;
|
|
80
|
+
pointer: ClientPointerContext = createClientPointerContext();
|
|
66
81
|
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
67
82
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
68
83
|
particleSettings: {
|
|
@@ -78,6 +93,7 @@ export class RpgClientEngine<T = any> {
|
|
|
78
93
|
spriteComponentsBehind = signal<any[]>([]);
|
|
79
94
|
spriteComponentsInFront = signal<any[]>([]);
|
|
80
95
|
spriteComponents: Map<string, any> = new Map();
|
|
96
|
+
private eventComponentResolvers = new EventComponentResolverRegistry();
|
|
81
97
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
82
98
|
cameraFollowTargetId = signal<string | null>(null);
|
|
83
99
|
/** Trigger for map shake animation */
|
|
@@ -93,6 +109,8 @@ export class RpgClientEngine<T = any> {
|
|
|
93
109
|
private pendingPredictionFrames: number[] = [];
|
|
94
110
|
private lastClientPhysicsStepAt = 0;
|
|
95
111
|
private frameOffset = 0;
|
|
112
|
+
private latestServerTick?: number;
|
|
113
|
+
private latestServerTickAt = 0;
|
|
96
114
|
// Ping/Pong for RTT measurement
|
|
97
115
|
private rtt: number = 0; // Round-trip time in ms
|
|
98
116
|
private pingInterval: any = null;
|
|
@@ -109,10 +127,16 @@ export class RpgClientEngine<T = any> {
|
|
|
109
127
|
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
110
128
|
private onAfterLoadingSubscription?: any;
|
|
111
129
|
private sceneResetQueued = false;
|
|
130
|
+
private mapTransitionInProgress = false;
|
|
131
|
+
private currentMapRoomId?: string;
|
|
132
|
+
private socketListenersInitialized = false;
|
|
112
133
|
|
|
113
134
|
// Store subscriptions and event listeners for cleanup
|
|
114
135
|
private tickSubscriptions: any[] = [];
|
|
115
136
|
private resizeHandler?: () => void;
|
|
137
|
+
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
138
|
+
private pointerCanvas?: HTMLCanvasElement;
|
|
139
|
+
private pendingSyncPackets: any[] = [];
|
|
116
140
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
117
141
|
|
|
118
142
|
constructor(public context) {
|
|
@@ -120,6 +144,10 @@ export class RpgClientEngine<T = any> {
|
|
|
120
144
|
this.guiService = inject(RpgGui);
|
|
121
145
|
this.loadMapService = inject(LoadMapToken);
|
|
122
146
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
147
|
+
this.projectiles = new ProjectileManager(
|
|
148
|
+
this.hooks,
|
|
149
|
+
(projectile) => this.predictProjectileImpact(projectile),
|
|
150
|
+
);
|
|
123
151
|
this.globalConfig = inject(GlobalConfigToken)
|
|
124
152
|
|
|
125
153
|
if (!this.globalConfig) {
|
|
@@ -197,6 +225,22 @@ export class RpgClientEngine<T = any> {
|
|
|
197
225
|
this.sceneMap = new RpgClientMap()
|
|
198
226
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
199
227
|
this.sceneMap.loadPhysic();
|
|
228
|
+
this.resolveSceneMapComponent();
|
|
229
|
+
|
|
230
|
+
const saveClient = inject(SaveClientService);
|
|
231
|
+
saveClient.initialize();
|
|
232
|
+
this.initListeners();
|
|
233
|
+
this.guiService._initialize();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await this.webSocket.connection();
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
this.stopPingPong();
|
|
240
|
+
await this.callConnectError(error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
200
244
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
201
245
|
|
|
202
246
|
const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
|
|
@@ -207,8 +251,10 @@ export class RpgClientEngine<T = any> {
|
|
|
207
251
|
);
|
|
208
252
|
this.canvasApp = app;
|
|
209
253
|
this.canvasElement = canvasElement;
|
|
210
|
-
this.renderer = app.renderer as PIXI.Renderer;
|
|
254
|
+
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
255
|
+
this.setupPointerTracking();
|
|
211
256
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
257
|
+
this.flushPendingSyncPackets();
|
|
212
258
|
|
|
213
259
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
214
260
|
if (Date.now() - this.lastInputTime > 100) {
|
|
@@ -224,12 +270,14 @@ export class RpgClientEngine<T = any> {
|
|
|
224
270
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
225
271
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
226
272
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
227
|
-
|
|
273
|
+
|
|
228
274
|
RpgSound.init(this);
|
|
229
275
|
RpgResource.init(this);
|
|
230
276
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
231
277
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
232
278
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
279
|
+
this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
|
|
280
|
+
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
233
281
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
234
282
|
|
|
235
283
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
@@ -242,6 +290,7 @@ export class RpgClientEngine<T = any> {
|
|
|
242
290
|
|
|
243
291
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
244
292
|
this.stepClientPhysicsTick();
|
|
293
|
+
this.projectiles.step();
|
|
245
294
|
this.flushPendingPredictedStates();
|
|
246
295
|
this.flushPendingMovePath();
|
|
247
296
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
@@ -255,13 +304,56 @@ export class RpgClientEngine<T = any> {
|
|
|
255
304
|
});
|
|
256
305
|
this.tickSubscriptions.push(tickSubscription);
|
|
257
306
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
this.startPingPong();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private resolveSceneMapComponent() {
|
|
311
|
+
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
312
|
+
const component = components[components.length - 1];
|
|
313
|
+
if (component) {
|
|
314
|
+
this.sceneMapComponent = component;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private setupPointerTracking() {
|
|
319
|
+
const renderer = this.renderer as any;
|
|
320
|
+
const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
|
|
321
|
+
|
|
322
|
+
if (!canvas || typeof canvas.addEventListener !== "function") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.pointerCanvas = canvas;
|
|
327
|
+
this.pointerMoveHandler = (event: PointerEvent) => {
|
|
328
|
+
const rect = canvas.getBoundingClientRect();
|
|
329
|
+
const screen = {
|
|
330
|
+
x: event.clientX - rect.left,
|
|
331
|
+
y: event.clientY - rect.top,
|
|
332
|
+
};
|
|
333
|
+
const viewport = this.findViewportInstance();
|
|
334
|
+
let world = screen;
|
|
335
|
+
|
|
336
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
337
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
338
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
339
|
+
} else if (viewport && typeof viewport.toLocal === "function") {
|
|
340
|
+
const point = viewport.toLocal(screen);
|
|
341
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.pointer.update(screen, world);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
canvas.addEventListener("pointermove", this.pointerMoveHandler);
|
|
348
|
+
canvas.addEventListener("pointerdown", this.pointerMoveHandler);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private findViewportInstance(): any {
|
|
352
|
+
const children = (this.canvasApp as any)?.stage?.children ?? [];
|
|
353
|
+
return children.find((child: any) => (
|
|
354
|
+
typeof child?.toWorld === "function"
|
|
355
|
+
|| child?.constructor?.name === "Viewport"
|
|
356
|
+
));
|
|
265
357
|
}
|
|
266
358
|
|
|
267
359
|
private prepareSyncPayload(data: any): any {
|
|
@@ -311,52 +403,15 @@ export class RpgClientEngine<T = any> {
|
|
|
311
403
|
}
|
|
312
404
|
|
|
313
405
|
private initListeners() {
|
|
314
|
-
this.
|
|
315
|
-
|
|
316
|
-
this.playerIdSignal.set(data.pId);
|
|
317
|
-
// Signal that player ID was received
|
|
318
|
-
this.playerIdReceived$.next(true);
|
|
319
|
-
}
|
|
406
|
+
if (this.socketListenersInitialized) return;
|
|
407
|
+
this.socketListenersInitialized = true;
|
|
320
408
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Apply client-side prediction filtering and server reconciliation
|
|
328
|
-
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
329
|
-
|
|
330
|
-
const ack = data?.ack;
|
|
331
|
-
const normalizedAck =
|
|
332
|
-
ack && typeof ack.frame === "number"
|
|
333
|
-
? this.normalizeAckWithSyncState(ack, data)
|
|
334
|
-
: undefined;
|
|
335
|
-
const payload = this.prepareSyncPayload(data);
|
|
336
|
-
load(this.sceneMap, payload, true);
|
|
337
|
-
|
|
338
|
-
if (normalizedAck) {
|
|
339
|
-
this.applyServerAck(normalizedAck);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (const playerId in payload.players ?? {}) {
|
|
343
|
-
const player = payload.players[playerId]
|
|
344
|
-
if (!player._param) continue
|
|
345
|
-
for (const param in player._param) {
|
|
346
|
-
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Check if players and events are present in sync data
|
|
351
|
-
const players = payload.players || this.sceneMap.players();
|
|
352
|
-
if (players && Object.keys(players).length > 0) {
|
|
353
|
-
this.playersReceived$.next(true);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const events = payload.events || this.sceneMap.events();
|
|
357
|
-
if (events !== undefined) {
|
|
358
|
-
this.eventsReceived$.next(true);
|
|
409
|
+
this.webSocket.on("sync", (data) => {
|
|
410
|
+
if (!this.tick) {
|
|
411
|
+
this.pendingSyncPackets.push(data);
|
|
412
|
+
return;
|
|
359
413
|
}
|
|
414
|
+
this.applySyncPacket(data);
|
|
360
415
|
});
|
|
361
416
|
|
|
362
417
|
// Handle pong responses for RTT measurement
|
|
@@ -368,6 +423,7 @@ export class RpgClientEngine<T = any> {
|
|
|
368
423
|
// This helps us estimate which server tick corresponds to each client input frame
|
|
369
424
|
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
|
|
370
425
|
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
426
|
+
this.updateServerTickEstimate(estimatedServerTickNow, now);
|
|
371
427
|
|
|
372
428
|
// Update frame offset (only if we have inputs to calibrate with)
|
|
373
429
|
if (this.inputFrameCounter > 0) {
|
|
@@ -378,10 +434,8 @@ export class RpgClientEngine<T = any> {
|
|
|
378
434
|
});
|
|
379
435
|
|
|
380
436
|
this.webSocket.on("changeMap", (data) => {
|
|
381
|
-
|
|
382
|
-
this.
|
|
383
|
-
// Reset camera follow to default (follow current player) when changing maps
|
|
384
|
-
this.cameraFollowTargetId.set(null);
|
|
437
|
+
const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
|
|
438
|
+
this.beginMapTransfer(nextMapId);
|
|
385
439
|
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
386
440
|
this.loadScene(data.mapId, transferToken);
|
|
387
441
|
});
|
|
@@ -395,6 +449,38 @@ export class RpgClientEngine<T = any> {
|
|
|
395
449
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
396
450
|
});
|
|
397
451
|
|
|
452
|
+
this.webSocket.on("clientVisual", (data) => {
|
|
453
|
+
this.playClientVisual(data);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
this.webSocket.on("projectile:spawnBatch", (data) => {
|
|
457
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
458
|
+
this.projectiles.spawnBatch(data?.projectiles ?? [], {
|
|
459
|
+
mapId: data?.mapId,
|
|
460
|
+
currentServerTick: this.estimateServerTick(),
|
|
461
|
+
tickDurationMs: this.getPhysicsTickDurationMs(),
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
this.webSocket.on("projectile:impactBatch", (data) => {
|
|
466
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
467
|
+
this.projectiles.impactBatch(data?.impacts ?? [], {
|
|
468
|
+
mapId: data?.mapId,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
this.webSocket.on("projectile:destroyBatch", (data) => {
|
|
473
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
474
|
+
this.projectiles.destroyBatch(data?.projectiles ?? [], {
|
|
475
|
+
mapId: data?.mapId,
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
this.webSocket.on("projectile:clear", (data) => {
|
|
480
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
481
|
+
this.projectiles.clear();
|
|
482
|
+
});
|
|
483
|
+
|
|
398
484
|
this.webSocket.on("notification", (data) => {
|
|
399
485
|
this.notificationManager.add(data);
|
|
400
486
|
});
|
|
@@ -505,10 +591,102 @@ export class RpgClientEngine<T = any> {
|
|
|
505
591
|
})
|
|
506
592
|
|
|
507
593
|
this.webSocket.on('error', (error) => {
|
|
508
|
-
this.
|
|
594
|
+
void this.callConnectError(error);
|
|
509
595
|
})
|
|
510
596
|
}
|
|
511
597
|
|
|
598
|
+
private beginMapTransfer(nextMapId?: string) {
|
|
599
|
+
this.mapTransitionInProgress = true;
|
|
600
|
+
this.currentMapRoomId = nextMapId;
|
|
601
|
+
this.sceneResetQueued = false;
|
|
602
|
+
this.clearClientPredictionStates();
|
|
603
|
+
this.sceneMap.weatherState.set(null);
|
|
604
|
+
this.sceneMap.lightingState.set(null);
|
|
605
|
+
this.sceneMap.clearLightSpots();
|
|
606
|
+
this.clearComponentAnimations();
|
|
607
|
+
this.projectiles.setMapId(nextMapId);
|
|
608
|
+
this.cameraFollowTargetId.set(null);
|
|
609
|
+
this.sceneMap.reset();
|
|
610
|
+
this.sceneMap.loadPhysic();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private clearComponentAnimations() {
|
|
614
|
+
this.componentAnimations.forEach((componentAnimation) => {
|
|
615
|
+
componentAnimation.instance?.clear?.();
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private shouldProcessProjectilePacket(data: any): boolean {
|
|
620
|
+
if (this.mapTransitionInProgress) return false;
|
|
621
|
+
const packetMapId = normalizeRoomMapId(
|
|
622
|
+
typeof data?.mapId === "string" ? data.mapId : undefined,
|
|
623
|
+
);
|
|
624
|
+
const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
|
|
625
|
+
return !packetMapId || !currentMapId || packetMapId === currentMapId;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private async callConnectError(error: any) {
|
|
629
|
+
await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private flushPendingSyncPackets() {
|
|
633
|
+
const packets = this.pendingSyncPackets;
|
|
634
|
+
this.pendingSyncPackets = [];
|
|
635
|
+
packets.forEach((packet) => this.applySyncPacket(packet));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private applySyncPacket(data: any) {
|
|
639
|
+
if (data.pId) {
|
|
640
|
+
this.playerIdSignal.set(data.pId);
|
|
641
|
+
// Signal that player ID was received
|
|
642
|
+
this.playerIdReceived$.next(true);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (this.sceneResetQueued) {
|
|
646
|
+
const weatherState = this.sceneMap.weatherState();
|
|
647
|
+
const lightingState = this.sceneMap.lightingState();
|
|
648
|
+
this.sceneMap.reset();
|
|
649
|
+
this.sceneMap.weatherState.set(weatherState);
|
|
650
|
+
this.sceneMap.lightingState.set(lightingState);
|
|
651
|
+
this.sceneMap.loadPhysic();
|
|
652
|
+
this.sceneResetQueued = false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Apply client-side prediction filtering and server reconciliation
|
|
656
|
+
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
657
|
+
|
|
658
|
+
const ack = data?.ack;
|
|
659
|
+
const normalizedAck =
|
|
660
|
+
ack && typeof ack.frame === "number"
|
|
661
|
+
? this.normalizeAckWithSyncState(ack, data)
|
|
662
|
+
: undefined;
|
|
663
|
+
const payload = this.prepareSyncPayload(data);
|
|
664
|
+
load(this.sceneMap, payload, true);
|
|
665
|
+
|
|
666
|
+
if (normalizedAck) {
|
|
667
|
+
this.applyServerAck(normalizedAck);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const playerId in payload.players ?? {}) {
|
|
671
|
+
const player = payload.players[playerId]
|
|
672
|
+
if (!player._param) continue
|
|
673
|
+
for (const param in player._param) {
|
|
674
|
+
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Check if players and events are present in sync data
|
|
679
|
+
const players = payload.players || this.sceneMap.players();
|
|
680
|
+
if (players && Object.keys(players).length > 0) {
|
|
681
|
+
this.playersReceived$.next(true);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const events = payload.events || this.sceneMap.events();
|
|
685
|
+
if (events !== undefined) {
|
|
686
|
+
this.eventsReceived$.next(true);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
512
690
|
/**
|
|
513
691
|
* Start periodic ping/pong for client-server synchronization
|
|
514
692
|
*
|
|
@@ -605,12 +783,15 @@ export class RpgClientEngine<T = any> {
|
|
|
605
783
|
room: mapId,
|
|
606
784
|
query: transferToken ? { transferToken } : undefined,
|
|
607
785
|
})
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.
|
|
613
|
-
|
|
786
|
+
try {
|
|
787
|
+
await this.webSocket.reconnect()
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
this.mapTransitionInProgress = false;
|
|
791
|
+
this.stopPingPong();
|
|
792
|
+
await this.callConnectError(error);
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
614
795
|
const res = await this.loadMapService.load(mapId)
|
|
615
796
|
this.sceneMap.data.set(res)
|
|
616
797
|
|
|
@@ -632,6 +813,8 @@ export class RpgClientEngine<T = any> {
|
|
|
632
813
|
|
|
633
814
|
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
634
815
|
this.mapLoadCompleted$.next(true);
|
|
816
|
+
this.currentMapRoomId = mapId;
|
|
817
|
+
this.mapTransitionInProgress = false;
|
|
635
818
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
636
819
|
this.sceneMap.loadPhysic()
|
|
637
820
|
}
|
|
@@ -1157,6 +1340,73 @@ export class RpgClientEngine<T = any> {
|
|
|
1157
1340
|
return this.spriteComponents.get(id);
|
|
1158
1341
|
}
|
|
1159
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* Register a custom event component resolver.
|
|
1345
|
+
*
|
|
1346
|
+
* The last resolver returning a component wins. This lets later modules
|
|
1347
|
+
* override earlier defaults without replacing the whole map scene.
|
|
1348
|
+
*
|
|
1349
|
+
* @param resolver - Function receiving the synced event object
|
|
1350
|
+
* @returns The registered resolver
|
|
1351
|
+
*/
|
|
1352
|
+
addEventComponentResolver(resolver: EventComponentResolver) {
|
|
1353
|
+
return this.eventComponentResolvers.add(resolver);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Resolve the custom CanvasEngine component for an event, if any.
|
|
1358
|
+
*
|
|
1359
|
+
* @param event - Synced client event object
|
|
1360
|
+
* @returns The component/config returned by the last matching resolver
|
|
1361
|
+
*/
|
|
1362
|
+
resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {
|
|
1363
|
+
return this.eventComponentResolvers.resolve(event);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
registerProjectileComponent(type: string, component: any) {
|
|
1367
|
+
return this.projectiles.register(type, component);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
getProjectileComponent(type: string) {
|
|
1371
|
+
return this.projectiles.get(type);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Register a named client visual macro.
|
|
1376
|
+
*
|
|
1377
|
+
* Client visuals are small client-side functions that group existing visual
|
|
1378
|
+
* primitives such as flash, sound, component animations, sprite animation, or
|
|
1379
|
+
* map shake. The server sends only the visual name and a serializable payload.
|
|
1380
|
+
*
|
|
1381
|
+
* @param name - Stable visual name sent by the server
|
|
1382
|
+
* @param handler - Client-side visual handler
|
|
1383
|
+
* @returns The registered handler
|
|
1384
|
+
*/
|
|
1385
|
+
registerClientVisual(name: string, handler: ClientVisualHandler) {
|
|
1386
|
+
return this.clientVisuals.register(name, handler);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Register several named client visual macros.
|
|
1391
|
+
*
|
|
1392
|
+
* @param visuals - Map of visual names to client-side handlers
|
|
1393
|
+
*/
|
|
1394
|
+
registerClientVisuals(visuals: ClientVisualMap) {
|
|
1395
|
+
this.clientVisuals.registerMany(visuals);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Play a registered client visual locally.
|
|
1400
|
+
*
|
|
1401
|
+
* This is also used by the websocket listener when the server calls
|
|
1402
|
+
* `player.clientVisual()` or `map.clientVisual()`.
|
|
1403
|
+
*
|
|
1404
|
+
* @param packet - Visual name and serializable payload
|
|
1405
|
+
*/
|
|
1406
|
+
playClientVisual(packet: ClientVisualPacket) {
|
|
1407
|
+
return this.clientVisuals.play(packet, this);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1160
1410
|
/**
|
|
1161
1411
|
* Add a component animation to the engine
|
|
1162
1412
|
*
|
|
@@ -1307,7 +1557,9 @@ export class RpgClientEngine<T = any> {
|
|
|
1307
1557
|
this.lastInputTime = Date.now();
|
|
1308
1558
|
}
|
|
1309
1559
|
|
|
1310
|
-
processAction(
|
|
1560
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
1561
|
+
processAction(action: RpgActionInput): void;
|
|
1562
|
+
processAction(action: RpgActionName | RpgActionInput, data?: any): void {
|
|
1311
1563
|
if (this.stopProcessingInput) return;
|
|
1312
1564
|
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1313
1565
|
const canMove =
|
|
@@ -1315,8 +1567,15 @@ export class RpgClientEngine<T = any> {
|
|
|
1315
1567
|
getCanMoveValue(currentPlayer);
|
|
1316
1568
|
if (!canMove) return;
|
|
1317
1569
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1570
|
+
const payload = normalizeActionInput(action as any, data);
|
|
1571
|
+
|
|
1572
|
+
this.hooks.callHooks("client-engine-onInput", this, {
|
|
1573
|
+
input: payload.action,
|
|
1574
|
+
action: payload.action,
|
|
1575
|
+
data: payload.data,
|
|
1576
|
+
playerId: this.playerId,
|
|
1577
|
+
}).subscribe();
|
|
1578
|
+
this.webSocket.emit('action', payload)
|
|
1320
1579
|
}
|
|
1321
1580
|
|
|
1322
1581
|
get PIXI() {
|
|
@@ -1335,8 +1594,76 @@ export class RpgClientEngine<T = any> {
|
|
|
1335
1594
|
return this.sceneMap
|
|
1336
1595
|
}
|
|
1337
1596
|
|
|
1597
|
+
getObjectById(id: string) {
|
|
1598
|
+
return this.sceneMap?.getObjectById(id);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1338
1601
|
private getPhysicsTick(): number {
|
|
1339
|
-
return this.sceneMap?.getTick?.() ?? 0;
|
|
1602
|
+
return (this.sceneMap as any)?.getTick?.() ?? 0;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private getPhysicsTickDurationMs(): number {
|
|
1606
|
+
const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
|
|
1607
|
+
return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
|
|
1608
|
+
? timeStep * 1000
|
|
1609
|
+
: 1000 / 60;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
|
|
1613
|
+
if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
this.latestServerTick = serverTick;
|
|
1617
|
+
this.latestServerTickAt = now;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
private estimateServerTick(now = Date.now()): number | undefined {
|
|
1621
|
+
if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
|
|
1625
|
+
return this.latestServerTick + elapsedTicks;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
|
|
1629
|
+
if (projectile.predictImpact === false) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
const sceneMap = this.sceneMap as any;
|
|
1633
|
+
if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
const origin = projectile.origin;
|
|
1637
|
+
const direction = projectile.direction;
|
|
1638
|
+
if (
|
|
1639
|
+
!origin ||
|
|
1640
|
+
!direction ||
|
|
1641
|
+
!Number.isFinite(origin.x) ||
|
|
1642
|
+
!Number.isFinite(origin.y) ||
|
|
1643
|
+
!Number.isFinite(direction.x) ||
|
|
1644
|
+
!Number.isFinite(direction.y) ||
|
|
1645
|
+
(direction.x === 0 && direction.y === 0)
|
|
1646
|
+
) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const hit = sceneMap.physic.raycast(
|
|
1651
|
+
new Vector2(origin.x, origin.y),
|
|
1652
|
+
new Vector2(direction.x, direction.y),
|
|
1653
|
+
projectile.range,
|
|
1654
|
+
projectile.collisionMask,
|
|
1655
|
+
(entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
|
|
1656
|
+
);
|
|
1657
|
+
if (!hit) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
id: projectile.id,
|
|
1662
|
+
targetId: hit.entity.uuid,
|
|
1663
|
+
x: hit.point.x,
|
|
1664
|
+
y: hit.point.y,
|
|
1665
|
+
distance: hit.distance,
|
|
1666
|
+
};
|
|
1340
1667
|
}
|
|
1341
1668
|
|
|
1342
1669
|
private ensureCurrentPlayerBody(): boolean {
|
|
@@ -1686,6 +2013,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1686
2013
|
}
|
|
1687
2014
|
|
|
1688
2015
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
2016
|
+
this.updateServerTickEstimate(ack.serverTick);
|
|
1689
2017
|
if (this.predictionEnabled && this.prediction) {
|
|
1690
2018
|
const result = this.prediction.applyServerAck({
|
|
1691
2019
|
frame: ack.frame,
|
|
@@ -1847,6 +2175,13 @@ export class RpgClientEngine<T = any> {
|
|
|
1847
2175
|
this.resizeHandler = undefined;
|
|
1848
2176
|
}
|
|
1849
2177
|
|
|
2178
|
+
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2179
|
+
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2180
|
+
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2181
|
+
this.pointerMoveHandler = undefined;
|
|
2182
|
+
this.pointerCanvas = undefined;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
1850
2185
|
// Destroy PIXI app and renderer if they exist
|
|
1851
2186
|
// Destroy the app first, which will destroy the renderer
|
|
1852
2187
|
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|
|
@@ -1920,6 +2255,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1920
2255
|
this.cameraFollowTargetId.set(null);
|
|
1921
2256
|
this.spriteComponentsBehind.set([]);
|
|
1922
2257
|
this.spriteComponentsInFront.set([]);
|
|
2258
|
+
this.eventComponentResolvers.clear();
|
|
1923
2259
|
|
|
1924
2260
|
// Clear maps and arrays
|
|
1925
2261
|
this.spritesheets.clear();
|