@rpgjs/client 5.0.0-alpha.23 → 5.0.0-alpha.25
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/dist/{index30.js → Game/AnimationManager.js} +1 -1
- package/dist/Game/AnimationManager.js.map +1 -0
- package/dist/{index24.js → Game/Event.js} +2 -2
- package/dist/Game/Event.js.map +1 -0
- package/dist/{index29.js → Game/Map.js} +6 -6
- package/dist/Game/Map.js.map +1 -0
- package/dist/{index22.js → Game/Object.js} +4 -4
- package/dist/Game/Object.js.map +1 -0
- package/dist/{index23.js → Game/Player.js} +2 -2
- package/dist/Game/Player.js.map +1 -0
- package/dist/{index9.js → Gui/Gui.js} +10 -11
- package/dist/{index9.js.map → Gui/Gui.js.map} +1 -1
- package/dist/{index19.js → Resource.js} +1 -1
- package/dist/Resource.js.map +1 -0
- package/dist/RpgClientEngine.d.ts +66 -115
- package/dist/{index2.js → RpgClientEngine.js} +234 -176
- package/dist/RpgClientEngine.js.map +1 -0
- package/dist/{index18.js → Sound.js} +1 -1
- package/dist/Sound.js.map +1 -0
- package/dist/{index35.js → components/animations/animation.ce.js} +3 -3
- package/dist/components/animations/animation.ce.js.map +1 -0
- package/dist/{index34.js → components/animations/hit.ce.js} +1 -1
- package/dist/components/animations/hit.ce.js.map +1 -0
- package/dist/{index12.js → components/animations/index.js} +3 -3
- package/dist/components/animations/index.js.map +1 -0
- package/dist/{index17.js → components/character.ce.js} +4 -4
- package/dist/components/character.ce.js.map +1 -0
- package/dist/{index51.js → components/dynamics/parse-value.js} +1 -1
- package/dist/components/dynamics/parse-value.js.map +1 -0
- package/dist/{index40.js → components/dynamics/text.ce.js} +2 -2
- package/dist/components/dynamics/text.ce.js.map +1 -0
- package/dist/{index11.js → components/gui/box.ce.js} +8 -8
- package/dist/components/gui/box.ce.js.map +1 -0
- package/dist/{index10.js → components/gui/dialogbox/index.ce.js} +4 -4
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -0
- package/dist/{index52.js → components/gui/dialogbox/itemMenu.ce.js} +1 -1
- package/dist/components/gui/dialogbox/itemMenu.ce.js.map +1 -0
- package/dist/{index47.js → components/gui/dialogbox/selection.ce.js} +4 -4
- package/dist/components/gui/dialogbox/selection.ce.js.map +1 -0
- package/dist/components/gui/mobile/index.d.ts +1 -1
- package/dist/{index25.js → components/gui/mobile/index.js} +5 -5
- package/dist/components/gui/mobile/index.js.map +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +17 -0
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -0
- package/dist/{index13.js → components/prebuilt/hp-bar.ce.js} +1 -1
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -0
- package/dist/{index14.js → components/prebuilt/light-halo.ce.js} +2 -3
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -0
- package/dist/{index26.js → components/scenes/canvas.ce.js} +6 -7
- package/dist/components/scenes/canvas.ce.js.map +1 -0
- package/dist/{index46.js → components/scenes/draw-map.ce.js} +3 -3
- package/dist/components/scenes/draw-map.ce.js.map +1 -0
- package/dist/{index16.js → components/scenes/event-layer.ce.js} +4 -4
- package/dist/components/scenes/event-layer.ce.js.map +1 -0
- package/dist/{index6.js → core/inject.js} +2 -2
- package/dist/core/inject.js.map +1 -0
- package/dist/{index5.js → core/setup.js} +4 -4
- package/dist/core/setup.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -24
- package/dist/index.js.map +1 -1
- package/dist/{index8.js → module.js} +10 -5
- package/dist/module.js.map +1 -0
- package/dist/{index20.js → node_modules/.pnpm/@signe_di@2.6.0/node_modules/@signe/di/dist/index.js} +1 -1
- package/dist/node_modules/.pnpm/@signe_di@2.6.0/node_modules/@signe/di/dist/index.js.map +1 -0
- package/dist/{index45.js → node_modules/.pnpm/@signe_reactive@2.6.0/node_modules/@signe/reactive/dist/index.js} +216 -11
- package/dist/node_modules/.pnpm/@signe_reactive@2.6.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
- package/dist/{index32.js → node_modules/.pnpm/@signe_room@2.6.0/node_modules/@signe/room/dist/index.js} +29 -30
- package/dist/node_modules/.pnpm/@signe_room@2.6.0/node_modules/@signe/room/dist/index.js.map +1 -0
- package/dist/{index42.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js} +1 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/{index33.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/client/index.js} +5 -5
- package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
- package/dist/{index28.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/index.js} +3 -3
- package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/index.js.map +1 -0
- package/dist/{index48.js → node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js} +1 -1
- package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +1 -0
- package/dist/{index43.js → node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js} +2 -15
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -0
- package/dist/{index44.js → node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js} +2 -5
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -0
- package/dist/{index50.js → node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js} +813 -100
- package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -0
- package/dist/{index36.js → presets/animation.js} +1 -1
- package/dist/presets/animation.js.map +1 -0
- package/dist/{index39.js → presets/faceset.js} +1 -1
- package/dist/presets/faceset.js.map +1 -0
- package/dist/presets/index.js +14 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/{index37.js → presets/lpc.js} +1 -1
- package/dist/presets/lpc.js.map +1 -0
- package/dist/{index38.js → presets/rmspritesheet.js} +1 -1
- package/dist/presets/rmspritesheet.js.map +1 -0
- package/dist/{index27.js → services/AbstractSocket.js} +1 -1
- package/dist/services/AbstractSocket.js.map +1 -0
- package/dist/{index21.js → services/keyboardControls.js} +1 -1
- package/dist/services/keyboardControls.js.map +1 -0
- package/dist/{index7.js → services/loadMap.js} +2 -2
- package/dist/services/loadMap.js.map +1 -0
- package/dist/{index4.js → services/mmorpg.js} +6 -6
- package/dist/services/mmorpg.js.map +1 -0
- package/dist/services/standalone.d.ts +3 -0
- package/dist/{index3.js → services/standalone.js} +18 -12
- package/dist/services/standalone.js.map +1 -0
- package/package.json +9 -9
- package/src/Gui/Gui.ts +0 -1
- package/src/RpgClientEngine.ts +298 -182
- package/src/components/character.ce +2 -3
- package/src/components/gui/mobile/index.ts +1 -1
- package/src/components/gui/mobile/mobile.ce +73 -88
- package/src/components/prebuilt/light-halo.ce +2 -71
- package/src/components/scenes/canvas.ce +0 -10
- package/src/components/scenes/event-layer.ce +1 -0
- package/src/index.ts +2 -1
- package/src/module.ts +6 -1
- package/src/services/standalone.ts +16 -7
- package/vite.config.ts +4 -2
- package/dist/Game/TransitionManager.d.ts +0 -56
- package/dist/index10.js.map +0 -1
- package/dist/index11.js.map +0 -1
- package/dist/index12.js.map +0 -1
- package/dist/index13.js.map +0 -1
- package/dist/index14.js.map +0 -1
- package/dist/index15.js +0 -14
- package/dist/index15.js.map +0 -1
- package/dist/index16.js.map +0 -1
- package/dist/index17.js.map +0 -1
- package/dist/index18.js.map +0 -1
- package/dist/index19.js.map +0 -1
- package/dist/index2.js.map +0 -1
- package/dist/index20.js.map +0 -1
- package/dist/index21.js.map +0 -1
- package/dist/index22.js.map +0 -1
- package/dist/index23.js.map +0 -1
- package/dist/index24.js.map +0 -1
- package/dist/index25.js.map +0 -1
- package/dist/index26.js.map +0 -1
- package/dist/index27.js.map +0 -1
- package/dist/index28.js.map +0 -1
- package/dist/index29.js.map +0 -1
- package/dist/index3.js.map +0 -1
- package/dist/index30.js.map +0 -1
- package/dist/index31.js +0 -53
- package/dist/index31.js.map +0 -1
- package/dist/index32.js.map +0 -1
- package/dist/index33.js.map +0 -1
- package/dist/index34.js.map +0 -1
- package/dist/index35.js.map +0 -1
- package/dist/index36.js.map +0 -1
- package/dist/index37.js.map +0 -1
- package/dist/index38.js.map +0 -1
- package/dist/index39.js.map +0 -1
- package/dist/index4.js.map +0 -1
- package/dist/index40.js.map +0 -1
- package/dist/index41.js +0 -43
- package/dist/index41.js.map +0 -1
- package/dist/index42.js.map +0 -1
- package/dist/index43.js.map +0 -1
- package/dist/index44.js.map +0 -1
- package/dist/index45.js.map +0 -1
- package/dist/index46.js.map +0 -1
- package/dist/index47.js.map +0 -1
- package/dist/index48.js.map +0 -1
- package/dist/index49.js +0 -7
- package/dist/index49.js.map +0 -1
- package/dist/index5.js.map +0 -1
- package/dist/index50.js.map +0 -1
- package/dist/index51.js.map +0 -1
- package/dist/index52.js.map +0 -1
- package/dist/index53.js +0 -6
- package/dist/index53.js.map +0 -1
- package/dist/index54.js +0 -12
- package/dist/index54.js.map +0 -1
- package/dist/index55.js +0 -113
- package/dist/index55.js.map +0 -1
- package/dist/index56.js +0 -136
- package/dist/index56.js.map +0 -1
- package/dist/index57.js +0 -137
- package/dist/index57.js.map +0 -1
- package/dist/index58.js +0 -112
- package/dist/index58.js.map +0 -1
- package/dist/index59.js +0 -9
- package/dist/index59.js.map +0 -1
- package/dist/index6.js.map +0 -1
- package/dist/index7.js.map +0 -1
- package/dist/index8.js.map +0 -1
- package/src/Game/TransitionManager.ts +0 -75
package/src/RpgClientEngine.ts
CHANGED
|
@@ -12,8 +12,7 @@ import { load } from "@signe/sync";
|
|
|
12
12
|
import { RpgClientMap } from "./Game/Map"
|
|
13
13
|
import { RpgGui } from "./Gui/Gui";
|
|
14
14
|
import { AnimationManager } from "./Game/AnimationManager";
|
|
15
|
-
import {
|
|
16
|
-
import { lastValueFrom, Observable } from "rxjs";
|
|
15
|
+
import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from "rxjs";
|
|
17
16
|
import { GlobalConfigToken } from "./module";
|
|
18
17
|
import * as PIXI from "pixi.js";
|
|
19
18
|
import { PrebuiltComponentAnimations } from "./components/animations";
|
|
@@ -37,7 +36,6 @@ export class RpgClientEngine<T = any> {
|
|
|
37
36
|
spritesheets: Map<string, any> = new Map();
|
|
38
37
|
sounds: Map<string, any> = new Map();
|
|
39
38
|
componentAnimations: any[] = [];
|
|
40
|
-
transitions: any[] = [];
|
|
41
39
|
private spritesheetResolver?: (id: string) => any | Promise<any>;
|
|
42
40
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
43
41
|
particleSettings: {
|
|
@@ -47,6 +45,8 @@ export class RpgClientEngine<T = any> {
|
|
|
47
45
|
}
|
|
48
46
|
renderer: PIXI.Renderer;
|
|
49
47
|
tick: Observable<number>;
|
|
48
|
+
private canvasApp?: any;
|
|
49
|
+
private canvasElement?: any;
|
|
50
50
|
playerIdSignal = signal<string | null>(null);
|
|
51
51
|
spriteComponentsBehind = signal<any[]>([]);
|
|
52
52
|
spriteComponentsInFront = signal<any[]>([]);
|
|
@@ -55,6 +55,8 @@ export class RpgClientEngine<T = any> {
|
|
|
55
55
|
/** Trigger for map shake animation */
|
|
56
56
|
mapShakeTrigger = trigger();
|
|
57
57
|
|
|
58
|
+
controlsReady = signal(undefined);
|
|
59
|
+
|
|
58
60
|
private predictionEnabled = false;
|
|
59
61
|
private prediction?: PredictionController<Direction>;
|
|
60
62
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
@@ -65,6 +67,16 @@ export class RpgClientEngine<T = any> {
|
|
|
65
67
|
private pingInterval: any = null;
|
|
66
68
|
private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
|
|
67
69
|
private lastInputTime = 0;
|
|
70
|
+
// Track map loading state for onAfterLoading hook using RxJS
|
|
71
|
+
private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);
|
|
72
|
+
private playerIdReceived$ = new BehaviorSubject<boolean>(false);
|
|
73
|
+
private playersReceived$ = new BehaviorSubject<boolean>(false);
|
|
74
|
+
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
75
|
+
private onAfterLoadingSubscription?: any;
|
|
76
|
+
|
|
77
|
+
// Store subscriptions and event listeners for cleanup
|
|
78
|
+
private tickSubscriptions: any[] = [];
|
|
79
|
+
private resizeHandler?: () => void;
|
|
68
80
|
|
|
69
81
|
constructor(public context) {
|
|
70
82
|
this.webSocket = inject(WebSocketToken);
|
|
@@ -134,6 +146,7 @@ export class RpgClientEngine<T = any> {
|
|
|
134
146
|
...currentValues,
|
|
135
147
|
values: new Map([['__default__', controlInstance]])
|
|
136
148
|
}
|
|
149
|
+
this.controlsReady.set(undefined);
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
async start() {
|
|
@@ -141,16 +154,19 @@ export class RpgClientEngine<T = any> {
|
|
|
141
154
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
142
155
|
|
|
143
156
|
const { app, canvasElement } = await bootstrapCanvas(this.selector, Canvas);
|
|
157
|
+
this.canvasApp = app;
|
|
158
|
+
this.canvasElement = canvasElement;
|
|
144
159
|
this.renderer = app.renderer as PIXI.Renderer;
|
|
145
160
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
146
161
|
|
|
147
|
-
this.tick.subscribe(() => {
|
|
162
|
+
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
148
163
|
if (Date.now() - this.lastInputTime > 100) {
|
|
149
164
|
const player = this.getCurrentPlayer();
|
|
150
165
|
if (!player) return;
|
|
151
166
|
(this.sceneMap as any).stopMovement(player);
|
|
152
167
|
}
|
|
153
|
-
})
|
|
168
|
+
});
|
|
169
|
+
this.tickSubscriptions.push(inputCheckSubscription);
|
|
154
170
|
|
|
155
171
|
|
|
156
172
|
this.hooks.callHooks("client-spritesheets-load", this).subscribe();
|
|
@@ -163,17 +179,17 @@ export class RpgClientEngine<T = any> {
|
|
|
163
179
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
164
180
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
165
181
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
166
|
-
this.hooks.callHooks("client-transitions-load", this).subscribe();
|
|
167
182
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
168
183
|
|
|
169
184
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
170
185
|
|
|
171
186
|
// wondow is resize
|
|
172
|
-
|
|
187
|
+
this.resizeHandler = () => {
|
|
173
188
|
this.hooks.callHooks("client-engine-onWindowResize", this).subscribe();
|
|
174
|
-
}
|
|
189
|
+
};
|
|
190
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
175
191
|
|
|
176
|
-
this.tick.subscribe((tick) => {
|
|
192
|
+
const tickSubscription = this.tick.subscribe((tick) => {
|
|
177
193
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
178
194
|
|
|
179
195
|
// Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)
|
|
@@ -182,7 +198,8 @@ export class RpgClientEngine<T = any> {
|
|
|
182
198
|
this.prediction?.cleanup(now);
|
|
183
199
|
this.prediction?.tryApplyPendingSnapshot();
|
|
184
200
|
}
|
|
185
|
-
})
|
|
201
|
+
});
|
|
202
|
+
this.tickSubscriptions.push(tickSubscription);
|
|
186
203
|
|
|
187
204
|
await this.webSocket.connection(() => {
|
|
188
205
|
this.initListeners()
|
|
@@ -192,12 +209,27 @@ export class RpgClientEngine<T = any> {
|
|
|
192
209
|
|
|
193
210
|
private initListeners() {
|
|
194
211
|
this.webSocket.on("sync", (data) => {
|
|
195
|
-
if (data.pId)
|
|
212
|
+
if (data.pId) {
|
|
213
|
+
this.playerIdSignal.set(data.pId);
|
|
214
|
+
// Signal that player ID was received
|
|
215
|
+
this.playerIdReceived$.next(true);
|
|
216
|
+
}
|
|
196
217
|
|
|
197
218
|
// Apply client-side prediction filtering and server reconciliation
|
|
198
219
|
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
199
220
|
|
|
200
221
|
load(this.sceneMap, data, true);
|
|
222
|
+
|
|
223
|
+
// Check if players and events are present in sync data
|
|
224
|
+
const players = data.players || this.sceneMap.players();
|
|
225
|
+
if (players && Object.keys(players).length > 0) {
|
|
226
|
+
this.playersReceived$.next(true);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const events = data.events || this.sceneMap.events();
|
|
230
|
+
if (events !== undefined) {
|
|
231
|
+
this.eventsReceived$.next(true);
|
|
232
|
+
}
|
|
201
233
|
});
|
|
202
234
|
|
|
203
235
|
// Handle pong responses for RTT measurement
|
|
@@ -269,7 +301,7 @@ export class RpgClientEngine<T = any> {
|
|
|
269
301
|
|
|
270
302
|
this.webSocket.on("shakeMap", (data) => {
|
|
271
303
|
const { intensity, duration, frequency, direction } = data || {};
|
|
272
|
-
this.mapShakeTrigger.start({
|
|
304
|
+
(this.mapShakeTrigger as any).start({
|
|
273
305
|
intensity,
|
|
274
306
|
duration,
|
|
275
307
|
frequency,
|
|
@@ -366,11 +398,25 @@ export class RpgClientEngine<T = any> {
|
|
|
366
398
|
}
|
|
367
399
|
|
|
368
400
|
private async loadScene(mapId: string) {
|
|
369
|
-
this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap)
|
|
401
|
+
await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
|
|
370
402
|
|
|
371
403
|
// Clear client prediction states when changing maps
|
|
372
404
|
this.clearClientPredictionStates();
|
|
373
405
|
|
|
406
|
+
// Reset all conditions for new map loading
|
|
407
|
+
this.mapLoadCompleted$.next(false);
|
|
408
|
+
this.playerIdReceived$.next(false);
|
|
409
|
+
this.playersReceived$.next(false);
|
|
410
|
+
this.eventsReceived$.next(false);
|
|
411
|
+
|
|
412
|
+
// Unsubscribe previous subscription if exists
|
|
413
|
+
if (this.onAfterLoadingSubscription) {
|
|
414
|
+
this.onAfterLoadingSubscription.unsubscribe();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Setup RxJS observable to wait for all conditions
|
|
418
|
+
this.setupOnAfterLoadingObserver();
|
|
419
|
+
|
|
374
420
|
this.webSocket.updateProperties({ room: mapId })
|
|
375
421
|
await this.webSocket.reconnect(() => {
|
|
376
422
|
this.initListeners()
|
|
@@ -378,7 +424,25 @@ export class RpgClientEngine<T = any> {
|
|
|
378
424
|
})
|
|
379
425
|
const res = await this.loadMapService.load(mapId)
|
|
380
426
|
this.sceneMap.data.set(res)
|
|
381
|
-
|
|
427
|
+
|
|
428
|
+
// Check if playerId is already present
|
|
429
|
+
if (this.playerIdSignal()) {
|
|
430
|
+
this.playerIdReceived$.next(true);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Check if players and events are already present in sceneMap
|
|
434
|
+
const players = this.sceneMap.players();
|
|
435
|
+
if (players && Object.keys(players).length > 0) {
|
|
436
|
+
this.playersReceived$.next(true);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const events = this.sceneMap.events();
|
|
440
|
+
if (events !== undefined) {
|
|
441
|
+
this.eventsReceived$.next(true);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
445
|
+
this.mapLoadCompleted$.next(true);
|
|
382
446
|
this.sceneMap.loadPhysic()
|
|
383
447
|
}
|
|
384
448
|
|
|
@@ -937,158 +1001,13 @@ export class RpgClientEngine<T = any> {
|
|
|
937
1001
|
return componentAnimation.instance
|
|
938
1002
|
}
|
|
939
1003
|
|
|
940
|
-
/**
|
|
941
|
-
* Add a transition to the engine
|
|
942
|
-
*
|
|
943
|
-
* Transitions are screen effects that can be displayed during scene changes,
|
|
944
|
-
* map loading, or any other moment where a visual transition is needed.
|
|
945
|
-
* They are displayed on top of the entire canvas and can have custom props
|
|
946
|
-
* that can be functions (similar to ComponentAnimation).
|
|
947
|
-
*
|
|
948
|
-
* @param transition - The transition configuration
|
|
949
|
-
* @param transition.id - Unique identifier for the transition
|
|
950
|
-
* @param transition.component - The component function to render
|
|
951
|
-
* @param transition.props - Optional props to pass to the component (can be a function)
|
|
952
|
-
* @returns The added transition configuration
|
|
953
|
-
*
|
|
954
|
-
* @example
|
|
955
|
-
* ```ts
|
|
956
|
-
* // Add a fade transition
|
|
957
|
-
* engine.addTransition({
|
|
958
|
-
* id: 'fade',
|
|
959
|
-
* component: FadeComponent
|
|
960
|
-
* });
|
|
961
|
-
*
|
|
962
|
-
* // Add a transition with props
|
|
963
|
-
* engine.addTransition({
|
|
964
|
-
* id: 'slide',
|
|
965
|
-
* component: SlideComponent,
|
|
966
|
-
* props: { direction: 'left', duration: 500 }
|
|
967
|
-
* });
|
|
968
|
-
*
|
|
969
|
-
* // Add a transition with function props
|
|
970
|
-
* engine.addTransition({
|
|
971
|
-
* id: 'custom',
|
|
972
|
-
* component: CustomTransition,
|
|
973
|
-
* props: (engine) => ({ width: engine.width(), height: engine.height() })
|
|
974
|
-
* });
|
|
975
|
-
* ```
|
|
976
|
-
*/
|
|
977
|
-
addTransition(transition: {
|
|
978
|
-
component: any,
|
|
979
|
-
id: string,
|
|
980
|
-
props?: any | ((engine: RpgClientEngine) => any)
|
|
981
|
-
}) {
|
|
982
|
-
const instance = new TransitionManager()
|
|
983
|
-
this.transitions.push({
|
|
984
|
-
id: transition.id,
|
|
985
|
-
component: transition.component,
|
|
986
|
-
props: transition.props,
|
|
987
|
-
instance: instance,
|
|
988
|
-
current: instance.current
|
|
989
|
-
})
|
|
990
|
-
return transition;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Remove a transition from the engine
|
|
995
|
-
*
|
|
996
|
-
* Removes a transition by its ID. This will not affect any currently
|
|
997
|
-
* running transitions, only prevent new ones from being started.
|
|
998
|
-
*
|
|
999
|
-
* @param id - The unique identifier of the transition to remove
|
|
1000
|
-
* @returns true if the transition was found and removed, false otherwise
|
|
1001
|
-
*
|
|
1002
|
-
* @example
|
|
1003
|
-
* ```ts
|
|
1004
|
-
* // Remove a transition
|
|
1005
|
-
* engine.removeTransition('fade');
|
|
1006
|
-
* ```
|
|
1007
|
-
*/
|
|
1008
|
-
removeTransition(id: string): boolean {
|
|
1009
|
-
const index = this.transitions.findIndex((transition) => transition.id === id);
|
|
1010
|
-
if (index !== -1) {
|
|
1011
|
-
this.transitions.splice(index, 1);
|
|
1012
|
-
return true;
|
|
1013
|
-
}
|
|
1014
|
-
return false;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Modify an existing transition
|
|
1019
|
-
*
|
|
1020
|
-
* Updates the component or props of an existing transition. This will
|
|
1021
|
-
* not affect any currently running transitions, only future ones.
|
|
1022
|
-
*
|
|
1023
|
-
* @param id - The unique identifier of the transition to modify
|
|
1024
|
-
* @param updates - The updates to apply (component and/or props)
|
|
1025
|
-
* @returns true if the transition was found and modified, false otherwise
|
|
1026
|
-
*
|
|
1027
|
-
* @example
|
|
1028
|
-
* ```ts
|
|
1029
|
-
* // Update transition props
|
|
1030
|
-
* engine.modifyTransition('fade', {
|
|
1031
|
-
* props: { duration: 2000, color: 'white' }
|
|
1032
|
-
* });
|
|
1033
|
-
*
|
|
1034
|
-
* // Update transition component
|
|
1035
|
-
* engine.modifyTransition('fade', {
|
|
1036
|
-
* component: NewFadeComponent
|
|
1037
|
-
* });
|
|
1038
|
-
* ```
|
|
1039
|
-
*/
|
|
1040
|
-
modifyTransition(id: string, updates: {
|
|
1041
|
-
component?: any,
|
|
1042
|
-
props?: any | ((engine: RpgClientEngine) => any)
|
|
1043
|
-
}): boolean {
|
|
1044
|
-
const transition = this.transitions.find((transition) => transition.id === id);
|
|
1045
|
-
if (!transition) {
|
|
1046
|
-
return false;
|
|
1047
|
-
}
|
|
1048
|
-
if (updates.component !== undefined) {
|
|
1049
|
-
transition.component = updates.component;
|
|
1050
|
-
}
|
|
1051
|
-
if (updates.props !== undefined) {
|
|
1052
|
-
transition.props = updates.props;
|
|
1053
|
-
}
|
|
1054
|
-
return true;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* Get a transition by its ID
|
|
1059
|
-
*
|
|
1060
|
-
* Retrieves the TransitionManager instance for a specific transition,
|
|
1061
|
-
* which can be used to start the transition.
|
|
1062
|
-
*
|
|
1063
|
-
* @param id - The unique identifier of the transition
|
|
1064
|
-
* @returns The TransitionManager instance for the transition
|
|
1065
|
-
* @throws Error if the transition is not found
|
|
1066
|
-
*
|
|
1067
|
-
* @example
|
|
1068
|
-
* ```ts
|
|
1069
|
-
* // Get a transition and start it
|
|
1070
|
-
* const fadeTransition = engine.getTransition('fade');
|
|
1071
|
-
* fadeTransition.start({ duration: 1000 });
|
|
1072
|
-
* ```
|
|
1073
|
-
*/
|
|
1074
|
-
getTransition(id: string): TransitionManager {
|
|
1075
|
-
const transition = this.transitions.find((transition) => transition.id === id)
|
|
1076
|
-
if (!transition) {
|
|
1077
|
-
throw new Error(`Transition with id ${id} not found`)
|
|
1078
|
-
}
|
|
1079
|
-
return transition.instance
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
1004
|
/**
|
|
1083
1005
|
* Start a transition
|
|
1084
1006
|
*
|
|
1085
|
-
* Convenience method to
|
|
1086
|
-
* getTransition and start into a single call. The transition will
|
|
1087
|
-
* automatically receive an onFinish callback to remove itself when done.
|
|
1007
|
+
* Convenience method to display a transition by its ID using the GUI system.
|
|
1088
1008
|
*
|
|
1089
1009
|
* @param id - The unique identifier of the transition to start
|
|
1090
|
-
* @param props -
|
|
1091
|
-
* @returns The created transition object
|
|
1010
|
+
* @param props - Props to pass to the transition component
|
|
1092
1011
|
*
|
|
1093
1012
|
* @example
|
|
1094
1013
|
* ```ts
|
|
@@ -1103,28 +1022,10 @@ export class RpgClientEngine<T = any> {
|
|
|
1103
1022
|
* ```
|
|
1104
1023
|
*/
|
|
1105
1024
|
startTransition(id: string, props: any = {}) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
throw new Error(`Transition with id ${id} not found`);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Get base props (can be a function or object)
|
|
1112
|
-
let baseProps = {};
|
|
1113
|
-
if (transition.props) {
|
|
1114
|
-
if (typeof transition.props === 'function') {
|
|
1115
|
-
baseProps = transition.props(this);
|
|
1116
|
-
} else {
|
|
1117
|
-
baseProps = transition.props;
|
|
1118
|
-
}
|
|
1025
|
+
if (!this.guiService.exists(id)) {
|
|
1026
|
+
throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
|
|
1119
1027
|
}
|
|
1120
|
-
|
|
1121
|
-
// Merge base props with provided props (provided props take precedence)
|
|
1122
|
-
const finalProps = {
|
|
1123
|
-
...baseProps,
|
|
1124
|
-
...props,
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
return transition.instance.start(finalProps);
|
|
1028
|
+
this.guiService.display(id, props);
|
|
1128
1029
|
}
|
|
1129
1030
|
|
|
1130
1031
|
async processInput({ input }: { input: Direction }) {
|
|
@@ -1230,6 +1131,46 @@ export class RpgClientEngine<T = any> {
|
|
|
1230
1131
|
return this.sceneMap.getCurrentPlayer()
|
|
1231
1132
|
}
|
|
1232
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* Setup RxJS observer to wait for all conditions before calling onAfterLoading hook
|
|
1136
|
+
*
|
|
1137
|
+
* This method uses RxJS `combineLatest` to wait for all conditions to be met,
|
|
1138
|
+
* regardless of the order in which they arrive:
|
|
1139
|
+
* 1. The map loading is completed (loadMapService.load is finished)
|
|
1140
|
+
* 2. We received a player ID (pId)
|
|
1141
|
+
* 3. Players array has at least one element
|
|
1142
|
+
* 4. Events property is present in the sync data
|
|
1143
|
+
*
|
|
1144
|
+
* Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.
|
|
1145
|
+
*
|
|
1146
|
+
* ## Design
|
|
1147
|
+
*
|
|
1148
|
+
* Uses BehaviorSubjects to track each condition state, allowing events to arrive
|
|
1149
|
+
* in any order. The `combineLatest` operator waits until all observables emit `true`,
|
|
1150
|
+
* then `take(1)` ensures the hook is called only once, and `switchMap` handles
|
|
1151
|
+
* the hook execution.
|
|
1152
|
+
*
|
|
1153
|
+
* @example
|
|
1154
|
+
* ```ts
|
|
1155
|
+
* // Called automatically in loadScene to setup the observer
|
|
1156
|
+
* this.setupOnAfterLoadingObserver();
|
|
1157
|
+
* ```
|
|
1158
|
+
*/
|
|
1159
|
+
private setupOnAfterLoadingObserver(): void {
|
|
1160
|
+
this.onAfterLoadingSubscription = combineLatest([
|
|
1161
|
+
this.mapLoadCompleted$.pipe(filter(completed => completed === true)),
|
|
1162
|
+
this.playerIdReceived$.pipe(filter(received => received === true)),
|
|
1163
|
+
this.playersReceived$.pipe(filter(received => received === true)),
|
|
1164
|
+
this.eventsReceived$.pipe(filter(received => received === true))
|
|
1165
|
+
]).pipe(
|
|
1166
|
+
take(1), // Only execute once when all conditions are met
|
|
1167
|
+
switchMap(() => {
|
|
1168
|
+
// Call the hook and return the observable
|
|
1169
|
+
return this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap);
|
|
1170
|
+
})
|
|
1171
|
+
).subscribe();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1233
1174
|
/**
|
|
1234
1175
|
* Clear client prediction states for cleanup
|
|
1235
1176
|
*
|
|
@@ -1364,4 +1305,179 @@ export class RpgClientEngine<T = any> {
|
|
|
1364
1305
|
private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {
|
|
1365
1306
|
// Prediction controller handles replay internally. Kept for backwards compatibility.
|
|
1366
1307
|
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Clear all client resources and reset state
|
|
1311
|
+
*
|
|
1312
|
+
* This method should be called to clean up all client-side resources when
|
|
1313
|
+
* shutting down or resetting the client engine. It:
|
|
1314
|
+
* - Destroys the PIXI renderer
|
|
1315
|
+
* - Stops all sounds
|
|
1316
|
+
* - Cleans up subscriptions and event listeners
|
|
1317
|
+
* - Resets scene map
|
|
1318
|
+
* - Stops ping/pong interval
|
|
1319
|
+
* - Clears prediction states
|
|
1320
|
+
*
|
|
1321
|
+
* ## Design
|
|
1322
|
+
*
|
|
1323
|
+
* This method is used primarily in testing environments to ensure clean
|
|
1324
|
+
* state between tests. In production, the client engine typically persists
|
|
1325
|
+
* for the lifetime of the application.
|
|
1326
|
+
*
|
|
1327
|
+
* @example
|
|
1328
|
+
* ```ts
|
|
1329
|
+
* // In test cleanup
|
|
1330
|
+
* afterEach(() => {
|
|
1331
|
+
* clientEngine.clear();
|
|
1332
|
+
* });
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
clear(): void {
|
|
1336
|
+
try {
|
|
1337
|
+
// First, unsubscribe from all tick subscriptions to stop rendering attempts
|
|
1338
|
+
for (const subscription of this.tickSubscriptions) {
|
|
1339
|
+
if (subscription && typeof subscription.unsubscribe === 'function') {
|
|
1340
|
+
subscription.unsubscribe();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
this.tickSubscriptions = [];
|
|
1344
|
+
|
|
1345
|
+
// Stop ping/pong interval
|
|
1346
|
+
if (this.pingInterval) {
|
|
1347
|
+
clearInterval(this.pingInterval);
|
|
1348
|
+
this.pingInterval = null;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Clean up onAfterLoading subscription
|
|
1352
|
+
if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {
|
|
1353
|
+
this.onAfterLoadingSubscription.unsubscribe();
|
|
1354
|
+
this.onAfterLoadingSubscription = undefined;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app
|
|
1358
|
+
// This prevents CanvasEngine from trying to render after PIXI is destroyed
|
|
1359
|
+
// CanvasEngine manages its own render loop which could try to access PIXI after destruction
|
|
1360
|
+
if (this.canvasElement) {
|
|
1361
|
+
try {
|
|
1362
|
+
// Try to stop or cleanup canvasElement if it has cleanup methods
|
|
1363
|
+
if (typeof (this.canvasElement as any).destroy === 'function') {
|
|
1364
|
+
(this.canvasElement as any).destroy();
|
|
1365
|
+
}
|
|
1366
|
+
// Clear the reference
|
|
1367
|
+
this.canvasElement = undefined;
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
// Ignore errors during canvasElement cleanup
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Reset scene map if it exists (this should stop any ongoing animations/renders)
|
|
1374
|
+
if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {
|
|
1375
|
+
(this.sceneMap as any).reset();
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Stop all sounds
|
|
1379
|
+
this.stopAllSounds();
|
|
1380
|
+
|
|
1381
|
+
// Remove resize event listener
|
|
1382
|
+
if (this.resizeHandler && typeof window !== 'undefined') {
|
|
1383
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1384
|
+
this.resizeHandler = undefined;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Destroy PIXI app and renderer if they exist
|
|
1388
|
+
// Destroy the app first, which will destroy the renderer
|
|
1389
|
+
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|
|
1390
|
+
const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';
|
|
1391
|
+
|
|
1392
|
+
if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {
|
|
1393
|
+
try {
|
|
1394
|
+
// Stop the ticker first to prevent any render calls during destruction
|
|
1395
|
+
if (this.canvasApp.ticker) {
|
|
1396
|
+
if (typeof this.canvasApp.ticker.stop === 'function') {
|
|
1397
|
+
this.canvasApp.ticker.stop();
|
|
1398
|
+
}
|
|
1399
|
+
// Also remove all listeners from ticker to prevent callbacks
|
|
1400
|
+
if (typeof this.canvasApp.ticker.removeAll === 'function') {
|
|
1401
|
+
this.canvasApp.ticker.removeAll();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Stop the renderer's ticker if it exists separately
|
|
1406
|
+
if (this.renderer && (this.renderer as any).ticker) {
|
|
1407
|
+
if (typeof (this.renderer as any).ticker.stop === 'function') {
|
|
1408
|
+
(this.renderer as any).ticker.stop();
|
|
1409
|
+
}
|
|
1410
|
+
if (typeof (this.renderer as any).ticker.removeAll === 'function') {
|
|
1411
|
+
(this.renderer as any).ticker.removeAll();
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Remove the canvas from DOM before destroying to prevent render attempts
|
|
1416
|
+
if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {
|
|
1417
|
+
this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Destroy with minimal options to avoid issues
|
|
1421
|
+
// Don't pass options that might trigger additional cleanup that could fail
|
|
1422
|
+
this.canvasApp.destroy(true);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
// Ignore errors during destruction
|
|
1425
|
+
}
|
|
1426
|
+
this.canvasApp = undefined;
|
|
1427
|
+
// canvasApp.destroy() already destroyed the renderer, so just null it
|
|
1428
|
+
this.renderer = null as any;
|
|
1429
|
+
} else if (rendererStillExists) {
|
|
1430
|
+
// Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed
|
|
1431
|
+
try {
|
|
1432
|
+
// Stop the renderer's ticker if it has one
|
|
1433
|
+
if ((this.renderer as any).ticker) {
|
|
1434
|
+
if (typeof (this.renderer as any).ticker.stop === 'function') {
|
|
1435
|
+
(this.renderer as any).ticker.stop();
|
|
1436
|
+
}
|
|
1437
|
+
if (typeof (this.renderer as any).ticker.removeAll === 'function') {
|
|
1438
|
+
(this.renderer as any).ticker.removeAll();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
this.renderer.destroy(true);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
// Ignore errors during destruction
|
|
1445
|
+
}
|
|
1446
|
+
this.renderer = null as any;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Clean up prediction controller
|
|
1450
|
+
if (this.prediction) {
|
|
1451
|
+
// Prediction controller cleanup is handled internally when destroyed
|
|
1452
|
+
this.prediction = undefined;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Reset signals
|
|
1456
|
+
this.playerIdSignal.set(null);
|
|
1457
|
+
this.cameraFollowTargetId.set(null);
|
|
1458
|
+
this.spriteComponentsBehind.set([]);
|
|
1459
|
+
this.spriteComponentsInFront.set([]);
|
|
1460
|
+
|
|
1461
|
+
// Clear maps and arrays
|
|
1462
|
+
this.spritesheets.clear();
|
|
1463
|
+
this.sounds.clear();
|
|
1464
|
+
this.componentAnimations = [];
|
|
1465
|
+
this.particleSettings.emitters = [];
|
|
1466
|
+
|
|
1467
|
+
// Reset state
|
|
1468
|
+
this.stopProcessingInput = false;
|
|
1469
|
+
this.lastInputTime = 0;
|
|
1470
|
+
this.inputFrameCounter = 0;
|
|
1471
|
+
this.frameOffset = 0;
|
|
1472
|
+
this.rtt = 0;
|
|
1473
|
+
|
|
1474
|
+
// Reset behavior subjects
|
|
1475
|
+
this.mapLoadCompleted$.next(false);
|
|
1476
|
+
this.playerIdReceived$.next(false);
|
|
1477
|
+
this.playersReceived$.next(false);
|
|
1478
|
+
this.eventsReceived$.next(false);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
console.warn('Error during client engine cleanup:', error);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1367
1483
|
}
|
|
@@ -13,15 +13,14 @@
|
|
|
13
13
|
tint
|
|
14
14
|
hitbox
|
|
15
15
|
flash={flashConfig}
|
|
16
|
-
|
|
17
|
-
/>
|
|
16
|
+
/>
|
|
18
17
|
}
|
|
19
18
|
</Container>
|
|
20
19
|
@for (compConfig of normalizedComponentsInFront) {
|
|
21
20
|
<Container dependencies={@compConfig.@dependencies}>
|
|
22
21
|
<compConfig.component object ...compConfig.props />
|
|
23
22
|
</Container>
|
|
24
|
-
}
|
|
23
|
+
}
|
|
25
24
|
@for (attachedGui of attachedGuis) {
|
|
26
25
|
@if (shouldDisplayAttachedGui) {
|
|
27
26
|
<Container>
|
|
@@ -16,7 +16,7 @@ export const withMobile = () => (
|
|
|
16
16
|
autoDisplay: true,
|
|
17
17
|
dependencies: () => {
|
|
18
18
|
const engine = inject(RpgClientEngine);
|
|
19
|
-
return [signal(isMobile() ||undefined), engine.
|
|
19
|
+
return [signal(isMobile() ||undefined), engine.controlsReady]
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
]
|