@rpgjs/client 5.0.0-beta.10 → 5.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/Game/ProjectileManager.d.ts +89 -0
- package/dist/Game/ProjectileManager.js +179 -0
- package/dist/Game/ProjectileManager.js.map +1 -0
- package/dist/Game/ProjectileManager.spec.d.ts +1 -0
- package/dist/RpgClient.d.ts +53 -13
- package/dist/RpgClientEngine.d.ts +25 -4
- package/dist/RpgClientEngine.js +197 -48
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/animations/hit.ce.js.map +1 -1
- package/dist/components/character.ce.js +32 -30
- 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 +3 -0
- package/dist/index.js +9 -5
- package/dist/module.js +11 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +12 -0
- package/dist/services/actionInput.js +27 -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.js +3 -2
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- package/package.json +7 -7
- package/src/Game/ProjectileManager.spec.ts +338 -0
- package/src/Game/ProjectileManager.ts +324 -0
- package/src/RpgClient.ts +62 -15
- package/src/RpgClientEngine.ts +287 -65
- package/src/components/character.ce +34 -32
- 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 +3 -0
- package/src/module.ts +13 -0
- package/src/services/actionInput.spec.ts +101 -0
- package/src/services/actionInput.ts +53 -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 +3 -2
package/src/RpgClient.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { ComponentFunction, Signal } from 'canvasengine'
|
|
|
2
2
|
import { RpgClientEngine } from './RpgClientEngine'
|
|
3
3
|
import { Loader, Container } from 'pixi.js'
|
|
4
4
|
import { RpgClientObject } from './Game/Object'
|
|
5
|
-
import { type MapPhysicsEntityContext, type MapPhysicsInitContext } from '@rpgjs/common'
|
|
5
|
+
import { type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
|
|
6
|
+
import type {
|
|
7
|
+
ClientProjectileSpawn,
|
|
8
|
+
RenderedProjectileProps,
|
|
9
|
+
} from './Game/ProjectileManager'
|
|
6
10
|
|
|
7
11
|
type RpgClass<T = any> = new (...args: any[]) => T
|
|
8
12
|
type RpgComponent = RpgClientObject
|
|
@@ -46,13 +50,14 @@ export interface RpgClientEngineHooks {
|
|
|
46
50
|
/**
|
|
47
51
|
* Recover keys from the pressed keyboard
|
|
48
52
|
*
|
|
49
|
-
* @prop { (engine: RpgClientEngine, obj: { input: string, playerId: number }) => any } [onInput]
|
|
53
|
+
* @prop { (engine: RpgClientEngine, obj: { input: string | number, action?: string | number, data?: any, playerId: number }) => any } [onInput]
|
|
50
54
|
* @memberof RpgEngineHooks
|
|
51
55
|
*/
|
|
52
|
-
onInput?: (engine: RpgClientEngine, obj: { input:
|
|
56
|
+
onInput?: (engine: RpgClientEngine, obj: { input: RpgActionName, action?: RpgActionName, data?: any, playerId: number }) => any
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* Called when the user is connected to the server
|
|
59
|
+
* Called when the user is connected to the server. In MMORPG mode, this
|
|
60
|
+
* runs after the server sends the RPGJS connection acceptance packet.
|
|
56
61
|
*
|
|
57
62
|
* @prop { (engine: RpgClientEngine, socket: any) => any } [onConnected]
|
|
58
63
|
* @memberof RpgEngineHooks
|
|
@@ -68,7 +73,8 @@ export interface RpgClientEngineHooks {
|
|
|
68
73
|
onDisconnect?: (engine: RpgClientEngine, reason: any, socket: any) => any
|
|
69
74
|
|
|
70
75
|
/**
|
|
71
|
-
* Called when there was a connection error
|
|
76
|
+
* Called when there was a connection error. In MMORPG mode, this also runs
|
|
77
|
+
* when server-side auth refuses the connection.
|
|
72
78
|
*
|
|
73
79
|
* @prop { (engine: RpgClientEngine, err: any, socket: any) => any } [onConnectError]
|
|
74
80
|
* @memberof RpgEngineHooks
|
|
@@ -245,6 +251,14 @@ export interface RpgSceneHooks<Scene> {
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
|
|
254
|
+
/**
|
|
255
|
+
* Root CanvasEngine component used to render the RPG scene map.
|
|
256
|
+
*
|
|
257
|
+
* Use the exported `SceneMap` component inside your custom component to
|
|
258
|
+
* keep the default map rendering and compose additional scene children.
|
|
259
|
+
*/
|
|
260
|
+
component?: ComponentFunction
|
|
261
|
+
|
|
248
262
|
/**
|
|
249
263
|
* The map and resources are being loaded
|
|
250
264
|
*
|
|
@@ -289,6 +303,28 @@ export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
|
|
|
289
303
|
onPhysicsReset?: (scene: SceneMap) => any
|
|
290
304
|
}
|
|
291
305
|
|
|
306
|
+
export interface RpgProjectileHooks {
|
|
307
|
+
/**
|
|
308
|
+
* CanvasEngine components used to render server-authoritative projectiles.
|
|
309
|
+
*/
|
|
310
|
+
components?: Record<string, ComponentFunction>
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Called when a projectile spawn batch is received from the server.
|
|
314
|
+
*/
|
|
315
|
+
onSpawn?: (projectile: ClientProjectileSpawn) => any
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Called when the server confirms a projectile impact.
|
|
319
|
+
*/
|
|
320
|
+
onImpact?: (projectile: RenderedProjectileProps | null) => any
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Called when the server destroys a projectile.
|
|
324
|
+
*/
|
|
325
|
+
onDestroy?: (projectile: RenderedProjectileProps | null) => any
|
|
326
|
+
}
|
|
327
|
+
|
|
292
328
|
export interface RpgClient {
|
|
293
329
|
/**
|
|
294
330
|
* Add hooks to the player or engine. All modules can listen to the hook
|
|
@@ -624,33 +660,36 @@ export interface RpgClient {
|
|
|
624
660
|
* */
|
|
625
661
|
sprite?: RpgSpriteHooks
|
|
626
662
|
|
|
627
|
-
/**
|
|
628
|
-
* Reference the scenes of the game.
|
|
663
|
+
/**
|
|
664
|
+
* Reference the scenes of the game.
|
|
629
665
|
*
|
|
630
666
|
* ```ts
|
|
631
667
|
* import { RpgSceneMapHooks, RpgClient, defineModule } from '@rpgjs/client'
|
|
668
|
+
* import MyScene from './my-scene.ce'
|
|
632
669
|
*
|
|
633
670
|
* export const sceneMap: RpgSceneMapHooks = {
|
|
634
|
-
*
|
|
671
|
+
* component: MyScene
|
|
635
672
|
* }
|
|
636
673
|
*
|
|
637
674
|
* defineModule<RpgClient>({
|
|
638
|
-
*
|
|
639
|
-
* // If you put the RpgSceneMap scene, Thhe key is called mandatory `map`
|
|
640
|
-
* map: sceneMap
|
|
641
|
-
* }
|
|
675
|
+
* sceneMap
|
|
642
676
|
* })
|
|
643
677
|
* ```
|
|
644
678
|
*
|
|
645
|
-
* @prop {
|
|
679
|
+
* @prop {RpgSceneMapHooks} [sceneMap]
|
|
646
680
|
* @memberof RpgClient
|
|
647
681
|
* */
|
|
682
|
+
sceneMap?: RpgSceneMapHooks
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Legacy scene map hook container.
|
|
686
|
+
*
|
|
687
|
+
* Prefer `sceneMap` for new code.
|
|
688
|
+
*/
|
|
648
689
|
scenes?: {
|
|
649
690
|
map: RpgSceneMapHooks
|
|
650
691
|
}
|
|
651
692
|
|
|
652
|
-
sceneMap?: RpgSceneMapHooks
|
|
653
|
-
|
|
654
693
|
/**
|
|
655
694
|
* Array containing the list of component animations
|
|
656
695
|
* Each element defines a temporary component to display for animations like hits, effects, etc.
|
|
@@ -681,4 +720,12 @@ export interface RpgClient {
|
|
|
681
720
|
id: string,
|
|
682
721
|
component: ComponentFunction
|
|
683
722
|
}[]
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Client-side projectile rendering configuration.
|
|
726
|
+
*
|
|
727
|
+
* Register a CanvasEngine component per projectile type. The server sends
|
|
728
|
+
* compact spawn/impact/destroy events and the client predicts x/y locally.
|
|
729
|
+
*/
|
|
730
|
+
projectiles?: RpgProjectileHooks
|
|
684
731
|
}
|
package/src/RpgClientEngine.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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";
|
|
9
10
|
import { load } from "@signe/sync";
|
|
10
11
|
import { RpgClientMap } from "./Game/Map"
|
|
11
12
|
import { RpgGui } from "./Gui/Gui";
|
|
@@ -22,10 +23,15 @@ import {
|
|
|
22
23
|
PredictionController,
|
|
23
24
|
type PredictionHistoryEntry,
|
|
24
25
|
type PredictionState,
|
|
26
|
+
type RpgActionInput,
|
|
27
|
+
type RpgActionName,
|
|
25
28
|
} from "@rpgjs/common";
|
|
26
29
|
import { NotificationManager } from "./Gui/NotificationManager";
|
|
27
30
|
import { SaveClientService } from "./services/save";
|
|
28
31
|
import { getCanMoveValue } from "./utils/readPropValue";
|
|
32
|
+
import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
|
|
33
|
+
import { normalizeActionInput } from "./services/actionInput";
|
|
34
|
+
import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
|
|
29
35
|
|
|
30
36
|
interface MovementTrajectoryPoint {
|
|
31
37
|
frame: number;
|
|
@@ -57,12 +63,15 @@ export class RpgClientEngine<T = any> {
|
|
|
57
63
|
private selector: HTMLElement;
|
|
58
64
|
public globalConfig: T;
|
|
59
65
|
public sceneComponent: any;
|
|
66
|
+
public sceneMapComponent: any = BuiltinSceneMap;
|
|
60
67
|
stopProcessingInput = false;
|
|
61
68
|
width = signal("100%");
|
|
62
69
|
height = signal("100%");
|
|
63
70
|
spritesheets: Map<string | number, any> = new Map();
|
|
64
71
|
sounds: Map<string, any> = new Map();
|
|
65
72
|
componentAnimations: any[] = [];
|
|
73
|
+
projectiles: ProjectileManager;
|
|
74
|
+
pointer: ClientPointerContext = createClientPointerContext();
|
|
66
75
|
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
67
76
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
68
77
|
particleSettings: {
|
|
@@ -93,6 +102,8 @@ export class RpgClientEngine<T = any> {
|
|
|
93
102
|
private pendingPredictionFrames: number[] = [];
|
|
94
103
|
private lastClientPhysicsStepAt = 0;
|
|
95
104
|
private frameOffset = 0;
|
|
105
|
+
private latestServerTick?: number;
|
|
106
|
+
private latestServerTickAt = 0;
|
|
96
107
|
// Ping/Pong for RTT measurement
|
|
97
108
|
private rtt: number = 0; // Round-trip time in ms
|
|
98
109
|
private pingInterval: any = null;
|
|
@@ -113,6 +124,9 @@ export class RpgClientEngine<T = any> {
|
|
|
113
124
|
// Store subscriptions and event listeners for cleanup
|
|
114
125
|
private tickSubscriptions: any[] = [];
|
|
115
126
|
private resizeHandler?: () => void;
|
|
127
|
+
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
128
|
+
private pointerCanvas?: HTMLCanvasElement;
|
|
129
|
+
private pendingSyncPackets: any[] = [];
|
|
116
130
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
117
131
|
|
|
118
132
|
constructor(public context) {
|
|
@@ -120,6 +134,10 @@ export class RpgClientEngine<T = any> {
|
|
|
120
134
|
this.guiService = inject(RpgGui);
|
|
121
135
|
this.loadMapService = inject(LoadMapToken);
|
|
122
136
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
137
|
+
this.projectiles = new ProjectileManager(
|
|
138
|
+
this.hooks,
|
|
139
|
+
(projectile) => this.predictProjectileImpact(projectile),
|
|
140
|
+
);
|
|
123
141
|
this.globalConfig = inject(GlobalConfigToken)
|
|
124
142
|
|
|
125
143
|
if (!this.globalConfig) {
|
|
@@ -197,6 +215,22 @@ export class RpgClientEngine<T = any> {
|
|
|
197
215
|
this.sceneMap = new RpgClientMap()
|
|
198
216
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
199
217
|
this.sceneMap.loadPhysic();
|
|
218
|
+
this.resolveSceneMapComponent();
|
|
219
|
+
|
|
220
|
+
const saveClient = inject(SaveClientService);
|
|
221
|
+
saveClient.initialize();
|
|
222
|
+
this.initListeners();
|
|
223
|
+
this.guiService._initialize();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await this.webSocket.connection();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.stopPingPong();
|
|
230
|
+
await this.callConnectError(error);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
|
|
200
234
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
201
235
|
|
|
202
236
|
const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
|
|
@@ -207,8 +241,10 @@ export class RpgClientEngine<T = any> {
|
|
|
207
241
|
);
|
|
208
242
|
this.canvasApp = app;
|
|
209
243
|
this.canvasElement = canvasElement;
|
|
210
|
-
this.renderer = app.renderer as PIXI.Renderer;
|
|
244
|
+
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
245
|
+
this.setupPointerTracking();
|
|
211
246
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
247
|
+
this.flushPendingSyncPackets();
|
|
212
248
|
|
|
213
249
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
214
250
|
if (Date.now() - this.lastInputTime > 100) {
|
|
@@ -224,12 +260,13 @@ export class RpgClientEngine<T = any> {
|
|
|
224
260
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
225
261
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
226
262
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
227
|
-
|
|
263
|
+
|
|
228
264
|
RpgSound.init(this);
|
|
229
265
|
RpgResource.init(this);
|
|
230
266
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
231
267
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
232
268
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
269
|
+
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
233
270
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
234
271
|
|
|
235
272
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
@@ -242,6 +279,7 @@ export class RpgClientEngine<T = any> {
|
|
|
242
279
|
|
|
243
280
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
244
281
|
this.stepClientPhysicsTick();
|
|
282
|
+
this.projectiles.step();
|
|
245
283
|
this.flushPendingPredictedStates();
|
|
246
284
|
this.flushPendingMovePath();
|
|
247
285
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
@@ -255,13 +293,56 @@ export class RpgClientEngine<T = any> {
|
|
|
255
293
|
});
|
|
256
294
|
this.tickSubscriptions.push(tickSubscription);
|
|
257
295
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
296
|
+
this.startPingPong();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private resolveSceneMapComponent() {
|
|
300
|
+
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
301
|
+
const component = components[components.length - 1];
|
|
302
|
+
if (component) {
|
|
303
|
+
this.sceneMapComponent = component;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private setupPointerTracking() {
|
|
308
|
+
const renderer = this.renderer as any;
|
|
309
|
+
const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
|
|
310
|
+
|
|
311
|
+
if (!canvas || typeof canvas.addEventListener !== "function") {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.pointerCanvas = canvas;
|
|
316
|
+
this.pointerMoveHandler = (event: PointerEvent) => {
|
|
317
|
+
const rect = canvas.getBoundingClientRect();
|
|
318
|
+
const screen = {
|
|
319
|
+
x: event.clientX - rect.left,
|
|
320
|
+
y: event.clientY - rect.top,
|
|
321
|
+
};
|
|
322
|
+
const viewport = this.findViewportInstance();
|
|
323
|
+
let world = screen;
|
|
324
|
+
|
|
325
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
326
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
327
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
328
|
+
} else if (viewport && typeof viewport.toLocal === "function") {
|
|
329
|
+
const point = viewport.toLocal(screen);
|
|
330
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.pointer.update(screen, world);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
canvas.addEventListener("pointermove", this.pointerMoveHandler);
|
|
337
|
+
canvas.addEventListener("pointerdown", this.pointerMoveHandler);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private findViewportInstance(): any {
|
|
341
|
+
const children = (this.canvasApp as any)?.stage?.children ?? [];
|
|
342
|
+
return children.find((child: any) => (
|
|
343
|
+
typeof child?.toWorld === "function"
|
|
344
|
+
|| child?.constructor?.name === "Viewport"
|
|
345
|
+
));
|
|
265
346
|
}
|
|
266
347
|
|
|
267
348
|
private prepareSyncPayload(data: any): any {
|
|
@@ -312,51 +393,11 @@ export class RpgClientEngine<T = any> {
|
|
|
312
393
|
|
|
313
394
|
private initListeners() {
|
|
314
395
|
this.webSocket.on("sync", (data) => {
|
|
315
|
-
if (
|
|
316
|
-
this.
|
|
317
|
-
|
|
318
|
-
this.playerIdReceived$.next(true);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (this.sceneResetQueued) {
|
|
322
|
-
this.sceneMap.reset();
|
|
323
|
-
this.sceneMap.loadPhysic();
|
|
324
|
-
this.sceneResetQueued = false;
|
|
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);
|
|
396
|
+
if (!this.tick) {
|
|
397
|
+
this.pendingSyncPackets.push(data);
|
|
398
|
+
return;
|
|
359
399
|
}
|
|
400
|
+
this.applySyncPacket(data);
|
|
360
401
|
});
|
|
361
402
|
|
|
362
403
|
// Handle pong responses for RTT measurement
|
|
@@ -368,6 +409,7 @@ export class RpgClientEngine<T = any> {
|
|
|
368
409
|
// This helps us estimate which server tick corresponds to each client input frame
|
|
369
410
|
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
|
|
370
411
|
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
412
|
+
this.updateServerTickEstimate(estimatedServerTickNow, now);
|
|
371
413
|
|
|
372
414
|
// Update frame offset (only if we have inputs to calibrate with)
|
|
373
415
|
if (this.inputFrameCounter > 0) {
|
|
@@ -379,7 +421,10 @@ export class RpgClientEngine<T = any> {
|
|
|
379
421
|
|
|
380
422
|
this.webSocket.on("changeMap", (data) => {
|
|
381
423
|
this.sceneResetQueued = true;
|
|
424
|
+
this.sceneMap.weatherState.set(null);
|
|
425
|
+
this.sceneMap.lightingState.set(null);
|
|
382
426
|
this.sceneMap.clearLightSpots();
|
|
427
|
+
this.projectiles.clear();
|
|
383
428
|
// Reset camera follow to default (follow current player) when changing maps
|
|
384
429
|
this.cameraFollowTargetId.set(null);
|
|
385
430
|
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
@@ -395,6 +440,25 @@ export class RpgClientEngine<T = any> {
|
|
|
395
440
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
396
441
|
});
|
|
397
442
|
|
|
443
|
+
this.webSocket.on("projectile:spawnBatch", (data) => {
|
|
444
|
+
this.projectiles.spawnBatch(data?.projectiles ?? [], {
|
|
445
|
+
currentServerTick: this.estimateServerTick(),
|
|
446
|
+
tickDurationMs: this.getPhysicsTickDurationMs(),
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
this.webSocket.on("projectile:impactBatch", (data) => {
|
|
451
|
+
this.projectiles.impactBatch(data?.impacts ?? []);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
this.webSocket.on("projectile:destroyBatch", (data) => {
|
|
455
|
+
this.projectiles.destroyBatch(data?.projectiles ?? []);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
this.webSocket.on("projectile:clear", () => {
|
|
459
|
+
this.projectiles.clear();
|
|
460
|
+
});
|
|
461
|
+
|
|
398
462
|
this.webSocket.on("notification", (data) => {
|
|
399
463
|
this.notificationManager.add(data);
|
|
400
464
|
});
|
|
@@ -505,10 +569,72 @@ export class RpgClientEngine<T = any> {
|
|
|
505
569
|
})
|
|
506
570
|
|
|
507
571
|
this.webSocket.on('error', (error) => {
|
|
508
|
-
this.
|
|
572
|
+
void this.callConnectError(error);
|
|
509
573
|
})
|
|
510
574
|
}
|
|
511
575
|
|
|
576
|
+
private async callConnectError(error: any) {
|
|
577
|
+
await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private flushPendingSyncPackets() {
|
|
581
|
+
const packets = this.pendingSyncPackets;
|
|
582
|
+
this.pendingSyncPackets = [];
|
|
583
|
+
packets.forEach((packet) => this.applySyncPacket(packet));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private applySyncPacket(data: any) {
|
|
587
|
+
if (data.pId) {
|
|
588
|
+
this.playerIdSignal.set(data.pId);
|
|
589
|
+
// Signal that player ID was received
|
|
590
|
+
this.playerIdReceived$.next(true);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (this.sceneResetQueued) {
|
|
594
|
+
const weatherState = this.sceneMap.weatherState();
|
|
595
|
+
const lightingState = this.sceneMap.lightingState();
|
|
596
|
+
this.sceneMap.reset();
|
|
597
|
+
this.sceneMap.weatherState.set(weatherState);
|
|
598
|
+
this.sceneMap.lightingState.set(lightingState);
|
|
599
|
+
this.sceneMap.loadPhysic();
|
|
600
|
+
this.sceneResetQueued = false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Apply client-side prediction filtering and server reconciliation
|
|
604
|
+
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
605
|
+
|
|
606
|
+
const ack = data?.ack;
|
|
607
|
+
const normalizedAck =
|
|
608
|
+
ack && typeof ack.frame === "number"
|
|
609
|
+
? this.normalizeAckWithSyncState(ack, data)
|
|
610
|
+
: undefined;
|
|
611
|
+
const payload = this.prepareSyncPayload(data);
|
|
612
|
+
load(this.sceneMap, payload, true);
|
|
613
|
+
|
|
614
|
+
if (normalizedAck) {
|
|
615
|
+
this.applyServerAck(normalizedAck);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const playerId in payload.players ?? {}) {
|
|
619
|
+
const player = payload.players[playerId]
|
|
620
|
+
if (!player._param) continue
|
|
621
|
+
for (const param in player._param) {
|
|
622
|
+
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check if players and events are present in sync data
|
|
627
|
+
const players = payload.players || this.sceneMap.players();
|
|
628
|
+
if (players && Object.keys(players).length > 0) {
|
|
629
|
+
this.playersReceived$.next(true);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const events = payload.events || this.sceneMap.events();
|
|
633
|
+
if (events !== undefined) {
|
|
634
|
+
this.eventsReceived$.next(true);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
512
638
|
/**
|
|
513
639
|
* Start periodic ping/pong for client-server synchronization
|
|
514
640
|
*
|
|
@@ -605,12 +731,19 @@ export class RpgClientEngine<T = any> {
|
|
|
605
731
|
room: mapId,
|
|
606
732
|
query: transferToken ? { transferToken } : undefined,
|
|
607
733
|
})
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
734
|
+
try {
|
|
735
|
+
await this.webSocket.reconnect(() => {
|
|
736
|
+
const saveClient = inject(SaveClientService);
|
|
737
|
+
saveClient.initialize();
|
|
738
|
+
this.initListeners()
|
|
739
|
+
this.guiService._initialize()
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
this.stopPingPong();
|
|
744
|
+
await this.callConnectError(error);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
614
747
|
const res = await this.loadMapService.load(mapId)
|
|
615
748
|
this.sceneMap.data.set(res)
|
|
616
749
|
|
|
@@ -1157,6 +1290,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1157
1290
|
return this.spriteComponents.get(id);
|
|
1158
1291
|
}
|
|
1159
1292
|
|
|
1293
|
+
registerProjectileComponent(type: string, component: any) {
|
|
1294
|
+
return this.projectiles.register(type, component);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
getProjectileComponent(type: string) {
|
|
1298
|
+
return this.projectiles.get(type);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1160
1301
|
/**
|
|
1161
1302
|
* Add a component animation to the engine
|
|
1162
1303
|
*
|
|
@@ -1307,7 +1448,9 @@ export class RpgClientEngine<T = any> {
|
|
|
1307
1448
|
this.lastInputTime = Date.now();
|
|
1308
1449
|
}
|
|
1309
1450
|
|
|
1310
|
-
processAction(
|
|
1451
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
1452
|
+
processAction(action: RpgActionInput): void;
|
|
1453
|
+
processAction(action: RpgActionName | RpgActionInput, data?: any): void {
|
|
1311
1454
|
if (this.stopProcessingInput) return;
|
|
1312
1455
|
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1313
1456
|
const canMove =
|
|
@@ -1315,8 +1458,15 @@ export class RpgClientEngine<T = any> {
|
|
|
1315
1458
|
getCanMoveValue(currentPlayer);
|
|
1316
1459
|
if (!canMove) return;
|
|
1317
1460
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1461
|
+
const payload = normalizeActionInput(action as any, data);
|
|
1462
|
+
|
|
1463
|
+
this.hooks.callHooks("client-engine-onInput", this, {
|
|
1464
|
+
input: payload.action,
|
|
1465
|
+
action: payload.action,
|
|
1466
|
+
data: payload.data,
|
|
1467
|
+
playerId: this.playerId,
|
|
1468
|
+
}).subscribe();
|
|
1469
|
+
this.webSocket.emit('action', payload)
|
|
1320
1470
|
}
|
|
1321
1471
|
|
|
1322
1472
|
get PIXI() {
|
|
@@ -1336,7 +1486,71 @@ export class RpgClientEngine<T = any> {
|
|
|
1336
1486
|
}
|
|
1337
1487
|
|
|
1338
1488
|
private getPhysicsTick(): number {
|
|
1339
|
-
return this.sceneMap?.getTick?.() ?? 0;
|
|
1489
|
+
return (this.sceneMap as any)?.getTick?.() ?? 0;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
private getPhysicsTickDurationMs(): number {
|
|
1493
|
+
const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
|
|
1494
|
+
return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
|
|
1495
|
+
? timeStep * 1000
|
|
1496
|
+
: 1000 / 60;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
|
|
1500
|
+
if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
this.latestServerTick = serverTick;
|
|
1504
|
+
this.latestServerTickAt = now;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
private estimateServerTick(now = Date.now()): number | undefined {
|
|
1508
|
+
if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
|
|
1509
|
+
return undefined;
|
|
1510
|
+
}
|
|
1511
|
+
const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
|
|
1512
|
+
return this.latestServerTick + elapsedTicks;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
|
|
1516
|
+
if (projectile.predictImpact === false) {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
const sceneMap = this.sceneMap as any;
|
|
1520
|
+
if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
const origin = projectile.origin;
|
|
1524
|
+
const direction = projectile.direction;
|
|
1525
|
+
if (
|
|
1526
|
+
!origin ||
|
|
1527
|
+
!direction ||
|
|
1528
|
+
!Number.isFinite(origin.x) ||
|
|
1529
|
+
!Number.isFinite(origin.y) ||
|
|
1530
|
+
!Number.isFinite(direction.x) ||
|
|
1531
|
+
!Number.isFinite(direction.y) ||
|
|
1532
|
+
(direction.x === 0 && direction.y === 0)
|
|
1533
|
+
) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const hit = sceneMap.physic.raycast(
|
|
1538
|
+
new Vector2(origin.x, origin.y),
|
|
1539
|
+
new Vector2(direction.x, direction.y),
|
|
1540
|
+
projectile.range,
|
|
1541
|
+
projectile.collisionMask,
|
|
1542
|
+
(entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
|
|
1543
|
+
);
|
|
1544
|
+
if (!hit) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
id: projectile.id,
|
|
1549
|
+
targetId: hit.entity.uuid,
|
|
1550
|
+
x: hit.point.x,
|
|
1551
|
+
y: hit.point.y,
|
|
1552
|
+
distance: hit.distance,
|
|
1553
|
+
};
|
|
1340
1554
|
}
|
|
1341
1555
|
|
|
1342
1556
|
private ensureCurrentPlayerBody(): boolean {
|
|
@@ -1686,6 +1900,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1686
1900
|
}
|
|
1687
1901
|
|
|
1688
1902
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
1903
|
+
this.updateServerTickEstimate(ack.serverTick);
|
|
1689
1904
|
if (this.predictionEnabled && this.prediction) {
|
|
1690
1905
|
const result = this.prediction.applyServerAck({
|
|
1691
1906
|
frame: ack.frame,
|
|
@@ -1847,6 +2062,13 @@ export class RpgClientEngine<T = any> {
|
|
|
1847
2062
|
this.resizeHandler = undefined;
|
|
1848
2063
|
}
|
|
1849
2064
|
|
|
2065
|
+
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2066
|
+
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2067
|
+
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2068
|
+
this.pointerMoveHandler = undefined;
|
|
2069
|
+
this.pointerCanvas = undefined;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1850
2072
|
// Destroy PIXI app and renderer if they exist
|
|
1851
2073
|
// Destroy the app first, which will destroy the renderer
|
|
1852
2074
|
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|