@rpgjs/client 5.0.0-alpha.22 → 5.0.0-alpha.24
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/Game/Object.d.ts +2 -0
- package/dist/RpgClientEngine.d.ts +115 -9
- package/dist/components/gui/mobile/index.d.ts +8 -0
- package/dist/components/prebuilt/index.d.ts +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -8
- package/dist/index.js.map +1 -1
- package/dist/index10.js +1 -1
- package/dist/index11.js +6 -5
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +2 -2
- package/dist/index13.js +102 -10
- package/dist/index13.js.map +1 -1
- package/dist/index14.js +67 -9
- package/dist/index14.js.map +1 -1
- package/dist/index15.js +10 -263
- package/dist/index15.js.map +1 -1
- package/dist/index16.js +9 -97
- package/dist/index16.js.map +1 -1
- package/dist/index17.js +300 -89
- package/dist/index17.js.map +1 -1
- package/dist/index18.js +63 -80
- package/dist/index18.js.map +1 -1
- package/dist/index19.js +96 -348
- package/dist/index19.js.map +1 -1
- package/dist/index2.js +176 -24
- package/dist/index2.js.map +1 -1
- package/dist/index20.js +360 -17
- package/dist/index20.js.map +1 -1
- package/dist/index21.js +19 -50
- package/dist/index21.js.map +1 -1
- package/dist/index22.js +212 -5
- package/dist/index22.js.map +1 -1
- package/dist/index23.js +6 -395
- package/dist/index23.js.map +1 -1
- package/dist/index24.js +4 -39
- package/dist/index24.js.map +1 -1
- package/dist/index25.js +19 -20
- package/dist/index25.js.map +1 -1
- package/dist/index26.js +43 -2624
- package/dist/index26.js.map +1 -1
- package/dist/index27.js +5 -110
- package/dist/index27.js.map +1 -1
- package/dist/index28.js +394 -65
- package/dist/index28.js.map +1 -1
- package/dist/index29.js +40 -15
- package/dist/index29.js.map +1 -1
- package/dist/index3.js +3 -3
- package/dist/index30.js +21 -23
- package/dist/index30.js.map +1 -1
- package/dist/index31.js +2624 -86
- package/dist/index31.js.map +1 -1
- package/dist/index32.js +107 -34
- package/dist/index32.js.map +1 -1
- package/dist/index33.js +69 -22
- package/dist/index33.js.map +1 -1
- package/dist/index34.js +19 -3
- package/dist/index34.js.map +1 -1
- package/dist/index35.js +21 -329
- package/dist/index35.js.map +1 -1
- package/dist/index36.js +91 -30
- package/dist/index36.js.map +1 -1
- package/dist/index37.js +37 -7
- package/dist/index37.js.map +1 -1
- package/dist/index38.js +22 -9
- package/dist/index38.js.map +1 -1
- package/dist/index39.js +139 -10
- package/dist/index39.js.map +1 -1
- package/dist/index4.js +3 -3
- package/dist/index40.js +16 -6
- package/dist/index40.js.map +1 -1
- package/dist/index41.js +1 -325
- package/dist/index41.js.map +1 -1
- package/dist/index42.js +530 -3680
- package/dist/index42.js.map +1 -1
- package/dist/index43.js +24 -67
- package/dist/index43.js.map +1 -1
- package/dist/index44.js +9 -184
- package/dist/index44.js.map +1 -1
- package/dist/index45.js +6 -503
- package/dist/index45.js.map +1 -1
- package/dist/index46.js +325 -2
- package/dist/index46.js.map +1 -1
- package/dist/index47.js +3687 -17
- package/dist/index47.js.map +1 -1
- package/dist/index48.js +69 -202
- package/dist/index48.js.map +1 -1
- package/dist/index49.js +182 -7
- package/dist/index49.js.map +1 -1
- package/dist/index5.js +1 -1
- package/dist/index50.js +497 -106
- package/dist/index50.js.map +1 -1
- package/dist/index51.js +48 -130
- package/dist/index51.js.map +1 -1
- package/dist/index52.js +17 -134
- package/dist/index52.js.map +1 -1
- package/dist/index53.js +3 -109
- package/dist/index53.js.map +1 -1
- package/dist/index54.js +9 -138
- package/dist/index54.js.map +1 -1
- package/dist/index55.js +111 -7
- package/dist/index55.js.map +1 -1
- package/dist/index56.js +130 -48
- package/dist/index56.js.map +1 -1
- package/dist/index57.js +137 -0
- package/dist/index57.js.map +1 -0
- package/dist/index58.js +112 -0
- package/dist/index58.js.map +1 -0
- package/dist/index59.js +9 -0
- package/dist/index59.js.map +1 -0
- package/dist/index6.js +1 -1
- package/dist/index7.js +1 -1
- package/dist/index8.js +17 -2
- package/dist/index8.js.map +1 -1
- package/dist/index9.js +10 -27
- package/dist/index9.js.map +1 -1
- package/dist/services/keyboardControls.d.ts +1 -2
- package/dist/services/mmorpg.d.ts +1 -1
- package/dist/services/standalone.d.ts +1 -1
- package/package.json +9 -9
- package/src/Game/Object.ts +8 -0
- package/src/Gui/Gui.ts +4 -31
- package/src/RpgClientEngine.ts +193 -20
- package/src/components/character.ce +146 -9
- package/src/components/gui/mobile/index.ts +24 -0
- package/src/components/gui/mobile/mobile.ce +80 -0
- package/src/components/prebuilt/index.ts +1 -0
- package/src/components/prebuilt/light-halo.ce +148 -0
- package/src/components/scenes/canvas.ce +2 -2
- package/src/components/scenes/event-layer.ce +1 -0
- package/src/components/scenes/transition.ce +60 -0
- package/src/index.ts +6 -1
- package/src/module.ts +15 -0
- package/src/services/keyboardControls.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/client",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.24",
|
|
4
4
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"pixi.js": "^8.9.2"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rpgjs/client": "5.0.0-alpha.
|
|
26
|
-
"@rpgjs/common": "5.0.0-alpha.
|
|
27
|
-
"@rpgjs/server": "5.0.0-alpha.
|
|
28
|
-
"@signe/di": "^2.
|
|
29
|
-
"@signe/room": "^2.
|
|
30
|
-
"@signe/sync": "^2.
|
|
25
|
+
"@rpgjs/client": "5.0.0-alpha.24",
|
|
26
|
+
"@rpgjs/common": "5.0.0-alpha.24",
|
|
27
|
+
"@rpgjs/server": "5.0.0-alpha.24",
|
|
28
|
+
"@signe/di": "^2.6.0",
|
|
29
|
+
"@signe/room": "^2.6.0",
|
|
30
|
+
"@signe/sync": "^2.6.0",
|
|
31
31
|
"pixi-filters": "^6.1.5",
|
|
32
32
|
"rxjs": "^7.8.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@canvasengine/compiler": "2.0.0-beta.
|
|
36
|
-
"vite": "^7.2.
|
|
35
|
+
"@canvasengine/compiler": "2.0.0-beta.40",
|
|
36
|
+
"vite": "^7.2.7",
|
|
37
37
|
"vite-plugin-dts": "^4.5.4",
|
|
38
38
|
"vitest": "^4.0.15"
|
|
39
39
|
},
|
package/src/Game/Object.ts
CHANGED
|
@@ -241,4 +241,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
241
241
|
const engine = inject(RpgClientEngine);
|
|
242
242
|
engine.getComponentAnimation(id).displayEffect(params, this);
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
isEvent(): boolean {
|
|
246
|
+
return this.type === 'event';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
isPlayer(): boolean {
|
|
250
|
+
return this.type === 'player';
|
|
251
|
+
}
|
|
244
252
|
}
|
package/src/Gui/Gui.ts
CHANGED
|
@@ -195,14 +195,13 @@ export class RpgGui {
|
|
|
195
195
|
if (!guiId) {
|
|
196
196
|
throw new Error("GUI must have a name or id");
|
|
197
197
|
}
|
|
198
|
-
|
|
199
198
|
const guiInstance: GuiInstance = {
|
|
200
199
|
name: guiId,
|
|
201
200
|
component: gui.component,
|
|
202
201
|
display: signal(gui.display || false),
|
|
203
202
|
data: signal(gui.data || {}),
|
|
204
203
|
autoDisplay: gui.autoDisplay || false,
|
|
205
|
-
dependencies: gui.dependencies,
|
|
204
|
+
dependencies: gui.dependencies ? gui.dependencies() : [],
|
|
206
205
|
attachToSprite: gui.attachToSprite || false,
|
|
207
206
|
};
|
|
208
207
|
|
|
@@ -317,8 +316,8 @@ export class RpgGui {
|
|
|
317
316
|
// Handle Vue component display
|
|
318
317
|
this._handleVueComponentDisplay(id, data, dependencies, guiInstance);
|
|
319
318
|
} else {
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
guiInstance.data.set(data);
|
|
320
|
+
guiInstance.display.set(true);
|
|
322
321
|
}
|
|
323
322
|
}
|
|
324
323
|
|
|
@@ -371,33 +370,7 @@ export class RpgGui {
|
|
|
371
370
|
* @param guiInstance - GUI instance
|
|
372
371
|
*/
|
|
373
372
|
private _handleCanvasComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {
|
|
374
|
-
|
|
375
|
-
if (guiInstance.subscription) {
|
|
376
|
-
guiInstance.subscription.unsubscribe();
|
|
377
|
-
guiInstance.subscription = undefined;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Use runtime dependencies or config dependencies
|
|
381
|
-
const deps = dependencies.length > 0
|
|
382
|
-
? dependencies
|
|
383
|
-
: (guiInstance.dependencies ? guiInstance.dependencies() : []);
|
|
384
|
-
|
|
385
|
-
if (deps.length > 0) {
|
|
386
|
-
// Subscribe to dependencies
|
|
387
|
-
guiInstance.subscription = combineLatest(
|
|
388
|
-
deps.map(dependency => dependency.observable)
|
|
389
|
-
).subscribe((values) => {
|
|
390
|
-
if (values.every(value => value !== undefined)) {
|
|
391
|
-
guiInstance.data.set(data);
|
|
392
|
-
guiInstance.display.set(true);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// No dependencies, display immediately
|
|
399
|
-
guiInstance.data.set(data);
|
|
400
|
-
guiInstance.display.set(true);
|
|
373
|
+
|
|
401
374
|
}
|
|
402
375
|
|
|
403
376
|
/**
|
package/src/RpgClientEngine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Canvas from "./components/scenes/canvas.ce";
|
|
2
|
-
import {
|
|
2
|
+
import { inject } from './core/inject'
|
|
3
3
|
import { signal, bootstrapCanvas, KeyboardControls, Howl, trigger } from "canvasengine";
|
|
4
4
|
import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
|
|
5
5
|
import { LoadMapService, LoadMapToken } from "./services/loadMap";
|
|
@@ -12,7 +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 { lastValueFrom, Observable } from "rxjs";
|
|
15
|
+
import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from "rxjs";
|
|
16
16
|
import { GlobalConfigToken } from "./module";
|
|
17
17
|
import * as PIXI from "pixi.js";
|
|
18
18
|
import { PrebuiltComponentAnimations } from "./components/animations";
|
|
@@ -53,6 +53,8 @@ export class RpgClientEngine<T = any> {
|
|
|
53
53
|
/** Trigger for map shake animation */
|
|
54
54
|
mapShakeTrigger = trigger();
|
|
55
55
|
|
|
56
|
+
controlsReady = signal(undefined);
|
|
57
|
+
|
|
56
58
|
private predictionEnabled = false;
|
|
57
59
|
private prediction?: PredictionController<Direction>;
|
|
58
60
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
@@ -63,13 +65,19 @@ export class RpgClientEngine<T = any> {
|
|
|
63
65
|
private pingInterval: any = null;
|
|
64
66
|
private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
|
|
65
67
|
private lastInputTime = 0;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// Track map loading state for onAfterLoading hook using RxJS
|
|
69
|
+
private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);
|
|
70
|
+
private playerIdReceived$ = new BehaviorSubject<boolean>(false);
|
|
71
|
+
private playersReceived$ = new BehaviorSubject<boolean>(false);
|
|
72
|
+
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
73
|
+
private onAfterLoadingSubscription?: any;
|
|
74
|
+
|
|
75
|
+
constructor(public context) {
|
|
76
|
+
this.webSocket = inject(WebSocketToken);
|
|
77
|
+
this.guiService = inject(RpgGui);
|
|
78
|
+
this.loadMapService = inject(LoadMapToken);
|
|
79
|
+
this.hooks = inject<Hooks>(ModulesToken);
|
|
80
|
+
this.globalConfig = inject(GlobalConfigToken)
|
|
73
81
|
|
|
74
82
|
if (!this.globalConfig) {
|
|
75
83
|
this.globalConfig = {} as T
|
|
@@ -127,11 +135,12 @@ export class RpgClientEngine<T = any> {
|
|
|
127
135
|
* ```
|
|
128
136
|
*/
|
|
129
137
|
setKeyboardControls(controlInstance: any) {
|
|
130
|
-
const currentValues = this.context.values['inject:' + KeyboardControls]
|
|
131
|
-
this.context.values['inject:' + KeyboardControls] = {
|
|
138
|
+
const currentValues = this.context.values['inject:' + 'KeyboardControls']
|
|
139
|
+
this.context.values['inject:' + 'KeyboardControls'] = {
|
|
132
140
|
...currentValues,
|
|
133
141
|
values: new Map([['__default__', controlInstance]])
|
|
134
142
|
}
|
|
143
|
+
this.controlsReady.set(true);
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
async start() {
|
|
@@ -189,12 +198,27 @@ export class RpgClientEngine<T = any> {
|
|
|
189
198
|
|
|
190
199
|
private initListeners() {
|
|
191
200
|
this.webSocket.on("sync", (data) => {
|
|
192
|
-
if (data.pId)
|
|
201
|
+
if (data.pId) {
|
|
202
|
+
this.playerIdSignal.set(data.pId);
|
|
203
|
+
// Signal that player ID was received
|
|
204
|
+
this.playerIdReceived$.next(true);
|
|
205
|
+
}
|
|
193
206
|
|
|
194
207
|
// Apply client-side prediction filtering and server reconciliation
|
|
195
208
|
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
196
209
|
|
|
197
210
|
load(this.sceneMap, data, true);
|
|
211
|
+
|
|
212
|
+
// Check if players and events are present in sync data
|
|
213
|
+
const players = data.players || this.sceneMap.players();
|
|
214
|
+
if (players && Object.keys(players).length > 0) {
|
|
215
|
+
this.playersReceived$.next(true);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const events = data.events || this.sceneMap.events();
|
|
219
|
+
if (events !== undefined) {
|
|
220
|
+
this.eventsReceived$.next(true);
|
|
221
|
+
}
|
|
198
222
|
});
|
|
199
223
|
|
|
200
224
|
// Handle pong responses for RTT measurement
|
|
@@ -266,7 +290,7 @@ export class RpgClientEngine<T = any> {
|
|
|
266
290
|
|
|
267
291
|
this.webSocket.on("shakeMap", (data) => {
|
|
268
292
|
const { intensity, duration, frequency, direction } = data || {};
|
|
269
|
-
this.mapShakeTrigger.start({
|
|
293
|
+
(this.mapShakeTrigger as any).start({
|
|
270
294
|
intensity,
|
|
271
295
|
duration,
|
|
272
296
|
frequency,
|
|
@@ -363,11 +387,25 @@ export class RpgClientEngine<T = any> {
|
|
|
363
387
|
}
|
|
364
388
|
|
|
365
389
|
private async loadScene(mapId: string) {
|
|
366
|
-
this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap)
|
|
390
|
+
await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
|
|
367
391
|
|
|
368
392
|
// Clear client prediction states when changing maps
|
|
369
393
|
this.clearClientPredictionStates();
|
|
370
394
|
|
|
395
|
+
// Reset all conditions for new map loading
|
|
396
|
+
this.mapLoadCompleted$.next(false);
|
|
397
|
+
this.playerIdReceived$.next(false);
|
|
398
|
+
this.playersReceived$.next(false);
|
|
399
|
+
this.eventsReceived$.next(false);
|
|
400
|
+
|
|
401
|
+
// Unsubscribe previous subscription if exists
|
|
402
|
+
if (this.onAfterLoadingSubscription) {
|
|
403
|
+
this.onAfterLoadingSubscription.unsubscribe();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Setup RxJS observable to wait for all conditions
|
|
407
|
+
this.setupOnAfterLoadingObserver();
|
|
408
|
+
|
|
371
409
|
this.webSocket.updateProperties({ room: mapId })
|
|
372
410
|
await this.webSocket.reconnect(() => {
|
|
373
411
|
this.initListeners()
|
|
@@ -375,7 +413,25 @@ export class RpgClientEngine<T = any> {
|
|
|
375
413
|
})
|
|
376
414
|
const res = await this.loadMapService.load(mapId)
|
|
377
415
|
this.sceneMap.data.set(res)
|
|
378
|
-
|
|
416
|
+
|
|
417
|
+
// Check if playerId is already present
|
|
418
|
+
if (this.playerIdSignal()) {
|
|
419
|
+
this.playerIdReceived$.next(true);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check if players and events are already present in sceneMap
|
|
423
|
+
const players = this.sceneMap.players();
|
|
424
|
+
if (players && Object.keys(players).length > 0) {
|
|
425
|
+
this.playersReceived$.next(true);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const events = this.sceneMap.events();
|
|
429
|
+
if (events !== undefined) {
|
|
430
|
+
this.eventsReceived$.next(true);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
434
|
+
this.mapLoadCompleted$.next(true);
|
|
379
435
|
this.sceneMap.loadPhysic()
|
|
380
436
|
}
|
|
381
437
|
|
|
@@ -787,13 +843,38 @@ export class RpgClientEngine<T = any> {
|
|
|
787
843
|
* Add a component to render behind sprites
|
|
788
844
|
* Components added with this method will be displayed with a lower z-index than the sprite
|
|
789
845
|
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
846
|
+
* Supports multiple formats:
|
|
847
|
+
* 1. Direct component: `ShadowComponent`
|
|
848
|
+
* 2. Configuration object: `{ component: LightHalo, props: {...} }`
|
|
849
|
+
* 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
|
|
850
|
+
* 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
|
|
851
|
+
*
|
|
852
|
+
* Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
|
|
853
|
+
* The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
|
|
854
|
+
*
|
|
855
|
+
* @param component - The component to add behind sprites, or a configuration object
|
|
856
|
+
* @param component.component - The component function to render
|
|
857
|
+
* @param component.props - Static props object or function that receives the sprite object and returns props
|
|
858
|
+
* @param component.dependencies - Function that receives the sprite object and returns an array of Signals
|
|
859
|
+
* @returns The added component or configuration
|
|
792
860
|
*
|
|
793
861
|
* @example
|
|
794
862
|
* ```ts
|
|
795
863
|
* // Add a shadow component behind all sprites
|
|
796
864
|
* engine.addSpriteComponentBehind(ShadowComponent);
|
|
865
|
+
*
|
|
866
|
+
* // Add a component with static props
|
|
867
|
+
* engine.addSpriteComponentBehind({
|
|
868
|
+
* component: LightHalo,
|
|
869
|
+
* props: { radius: 30 }
|
|
870
|
+
* });
|
|
871
|
+
*
|
|
872
|
+
* // Add a component with dynamic props and dependencies
|
|
873
|
+
* engine.addSpriteComponentBehind({
|
|
874
|
+
* component: HealthBar,
|
|
875
|
+
* props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
|
|
876
|
+
* dependencies: (object) => [object.hp, object.param.maxHp]
|
|
877
|
+
* });
|
|
797
878
|
* ```
|
|
798
879
|
*/
|
|
799
880
|
addSpriteComponentBehind(component: any) {
|
|
@@ -805,16 +886,41 @@ export class RpgClientEngine<T = any> {
|
|
|
805
886
|
* Add a component to render in front of sprites
|
|
806
887
|
* Components added with this method will be displayed with a higher z-index than the sprite
|
|
807
888
|
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
889
|
+
* Supports multiple formats:
|
|
890
|
+
* 1. Direct component: `HealthBarComponent`
|
|
891
|
+
* 2. Configuration object: `{ component: StatusIndicator, props: {...} }`
|
|
892
|
+
* 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`
|
|
893
|
+
* 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
|
|
894
|
+
*
|
|
895
|
+
* Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
|
|
896
|
+
* The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
|
|
897
|
+
*
|
|
898
|
+
* @param component - The component to add in front of sprites, or a configuration object
|
|
899
|
+
* @param component.component - The component function to render
|
|
900
|
+
* @param component.props - Static props object or function that receives the sprite object and returns props
|
|
901
|
+
* @param component.dependencies - Function that receives the sprite object and returns an array of Signals
|
|
902
|
+
* @returns The added component or configuration
|
|
810
903
|
*
|
|
811
904
|
* @example
|
|
812
905
|
* ```ts
|
|
813
906
|
* // Add a health bar component in front of all sprites
|
|
814
907
|
* engine.addSpriteComponentInFront(HealthBarComponent);
|
|
908
|
+
*
|
|
909
|
+
* // Add a component with static props
|
|
910
|
+
* engine.addSpriteComponentInFront({
|
|
911
|
+
* component: StatusIndicator,
|
|
912
|
+
* props: { type: 'poison' }
|
|
913
|
+
* });
|
|
914
|
+
*
|
|
915
|
+
* // Add a component with dynamic props and dependencies
|
|
916
|
+
* engine.addSpriteComponentInFront({
|
|
917
|
+
* component: HealthBar,
|
|
918
|
+
* props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
|
|
919
|
+
* dependencies: (object) => [object.hp, object.param.maxHp]
|
|
920
|
+
* });
|
|
815
921
|
* ```
|
|
816
922
|
*/
|
|
817
|
-
addSpriteComponentInFront(component: any) {
|
|
923
|
+
addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {
|
|
818
924
|
this.spriteComponentsInFront.update((components: any[]) => [...components, component])
|
|
819
925
|
return component
|
|
820
926
|
}
|
|
@@ -884,6 +990,33 @@ export class RpgClientEngine<T = any> {
|
|
|
884
990
|
return componentAnimation.instance
|
|
885
991
|
}
|
|
886
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Start a transition
|
|
995
|
+
*
|
|
996
|
+
* Convenience method to display a transition by its ID using the GUI system.
|
|
997
|
+
*
|
|
998
|
+
* @param id - The unique identifier of the transition to start
|
|
999
|
+
* @param props - Props to pass to the transition component
|
|
1000
|
+
*
|
|
1001
|
+
* @example
|
|
1002
|
+
* ```ts
|
|
1003
|
+
* // Start a fade transition
|
|
1004
|
+
* engine.startTransition('fade', { duration: 1000, color: 'black' });
|
|
1005
|
+
*
|
|
1006
|
+
* // Start with onFinish callback
|
|
1007
|
+
* engine.startTransition('fade', {
|
|
1008
|
+
* duration: 1000,
|
|
1009
|
+
* onFinish: () => console.log('Fade complete')
|
|
1010
|
+
* });
|
|
1011
|
+
* ```
|
|
1012
|
+
*/
|
|
1013
|
+
startTransition(id: string, props: any = {}) {
|
|
1014
|
+
if (!this.guiService.exists(id)) {
|
|
1015
|
+
throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
|
|
1016
|
+
}
|
|
1017
|
+
this.guiService.display(id, props);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
887
1020
|
async processInput({ input }: { input: Direction }) {
|
|
888
1021
|
const timestamp = Date.now();
|
|
889
1022
|
let frame: number;
|
|
@@ -987,6 +1120,46 @@ export class RpgClientEngine<T = any> {
|
|
|
987
1120
|
return this.sceneMap.getCurrentPlayer()
|
|
988
1121
|
}
|
|
989
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Setup RxJS observer to wait for all conditions before calling onAfterLoading hook
|
|
1125
|
+
*
|
|
1126
|
+
* This method uses RxJS `combineLatest` to wait for all conditions to be met,
|
|
1127
|
+
* regardless of the order in which they arrive:
|
|
1128
|
+
* 1. The map loading is completed (loadMapService.load is finished)
|
|
1129
|
+
* 2. We received a player ID (pId)
|
|
1130
|
+
* 3. Players array has at least one element
|
|
1131
|
+
* 4. Events property is present in the sync data
|
|
1132
|
+
*
|
|
1133
|
+
* Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.
|
|
1134
|
+
*
|
|
1135
|
+
* ## Design
|
|
1136
|
+
*
|
|
1137
|
+
* Uses BehaviorSubjects to track each condition state, allowing events to arrive
|
|
1138
|
+
* in any order. The `combineLatest` operator waits until all observables emit `true`,
|
|
1139
|
+
* then `take(1)` ensures the hook is called only once, and `switchMap` handles
|
|
1140
|
+
* the hook execution.
|
|
1141
|
+
*
|
|
1142
|
+
* @example
|
|
1143
|
+
* ```ts
|
|
1144
|
+
* // Called automatically in loadScene to setup the observer
|
|
1145
|
+
* this.setupOnAfterLoadingObserver();
|
|
1146
|
+
* ```
|
|
1147
|
+
*/
|
|
1148
|
+
private setupOnAfterLoadingObserver(): void {
|
|
1149
|
+
this.onAfterLoadingSubscription = combineLatest([
|
|
1150
|
+
this.mapLoadCompleted$.pipe(filter(completed => completed === true)),
|
|
1151
|
+
this.playerIdReceived$.pipe(filter(received => received === true)),
|
|
1152
|
+
this.playersReceived$.pipe(filter(received => received === true)),
|
|
1153
|
+
this.eventsReceived$.pipe(filter(received => received === true))
|
|
1154
|
+
]).pipe(
|
|
1155
|
+
take(1), // Only execute once when all conditions are met
|
|
1156
|
+
switchMap(() => {
|
|
1157
|
+
// Call the hook and return the observable
|
|
1158
|
+
return this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap);
|
|
1159
|
+
})
|
|
1160
|
+
).subscribe();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
990
1163
|
/**
|
|
991
1164
|
* Clear client prediction states for cleanup
|
|
992
1165
|
*
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
<Container x={smoothX} y={smoothY} zIndex={y} viewportFollow={shouldFollowCamera} controls onBeforeDestroy visible >
|
|
2
|
-
@for (
|
|
2
|
+
@for (compConfig of normalizedComponentsBehind) {
|
|
3
3
|
<Container>
|
|
4
|
-
<component object />
|
|
4
|
+
<compConfig.component object ...compConfig.props />
|
|
5
5
|
</Container>
|
|
6
6
|
}
|
|
7
7
|
<Particle emit={@emitParticleTrigger} settings={@particleSettings} zIndex={1000} name={particleName} />
|
|
8
8
|
<Container>
|
|
9
9
|
@for (graphicObj of graphicsSignals) {
|
|
10
|
-
<Sprite
|
|
10
|
+
<Sprite
|
|
11
|
+
sheet={@sheet(@graphicObj)}
|
|
12
|
+
direction
|
|
13
|
+
tint
|
|
14
|
+
hitbox
|
|
15
|
+
flash={flashConfig}
|
|
16
|
+
/>
|
|
11
17
|
}
|
|
12
18
|
</Container>
|
|
13
|
-
@for (
|
|
14
|
-
<Container>
|
|
15
|
-
<component object />
|
|
19
|
+
@for (compConfig of normalizedComponentsInFront) {
|
|
20
|
+
<Container dependencies={@compConfig.@dependencies}>
|
|
21
|
+
<compConfig.component object ...compConfig.props />
|
|
16
22
|
</Container>
|
|
17
|
-
}
|
|
23
|
+
}
|
|
18
24
|
@for (attachedGui of attachedGuis) {
|
|
19
25
|
@if (shouldDisplayAttachedGui) {
|
|
20
26
|
<Container>
|
|
21
|
-
<attachedGui.component ...attachedGui.data() object={object} onFinish={(data) => {
|
|
27
|
+
<attachedGui.component ...attachedGui.data() dependencies={@attachedGui.@dependencies} object={object} onFinish={(data) => {
|
|
22
28
|
onAttachedGuiFinish(attachedGui, data)
|
|
23
29
|
}} onInteraction={(name, data) => {
|
|
24
30
|
onAttachedGuiInteraction(attachedGui, name, data)
|
|
@@ -52,6 +58,137 @@
|
|
|
52
58
|
const componentsBehind = client.spriteComponentsBehind;
|
|
53
59
|
const componentsInFront = client.spriteComponentsInFront;
|
|
54
60
|
const isMe = computed(() => id() === playerId);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a single sprite component configuration
|
|
64
|
+
*
|
|
65
|
+
* Handles both direct component references and configuration objects with optional props and dependencies.
|
|
66
|
+
* Extracts the component reference and creates a computed function that returns the props.
|
|
67
|
+
*
|
|
68
|
+
* ## Design
|
|
69
|
+
*
|
|
70
|
+
* Supports two formats:
|
|
71
|
+
* 1. Direct component: `ShadowComponent`
|
|
72
|
+
* 2. Configuration object: `{ component: LightHalo, props: {...}, dependencies: (object) => [...] }`
|
|
73
|
+
*
|
|
74
|
+
* The normalization process:
|
|
75
|
+
* - Extracts the actual component from either format
|
|
76
|
+
* - Extracts dependencies function if provided
|
|
77
|
+
* - Creates a computed function that returns props (static object or dynamic function result)
|
|
78
|
+
* - Returns a normalized object with `component`, `props`, and `dependencies`
|
|
79
|
+
*
|
|
80
|
+
* @param comp - Component reference or configuration object
|
|
81
|
+
* @returns Normalized component configuration with component, props, and dependencies
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // Direct component
|
|
86
|
+
* normalizeComponent(ShadowComponent)
|
|
87
|
+
* // => { component: ShadowComponent, props: {}, dependencies: undefined }
|
|
88
|
+
*
|
|
89
|
+
* // With static props
|
|
90
|
+
* normalizeComponent({ component: LightHalo, props: { radius: 30 } })
|
|
91
|
+
* // => { component: LightHalo, props: { radius: 30 }, dependencies: undefined }
|
|
92
|
+
*
|
|
93
|
+
* // With dynamic props and dependencies
|
|
94
|
+
* normalizeComponent({
|
|
95
|
+
* component: HealthBar,
|
|
96
|
+
* props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
|
|
97
|
+
* dependencies: (object) => [object.hp, object.param.maxHp]
|
|
98
|
+
* })
|
|
99
|
+
* // => { component: HealthBar, props: {...}, dependencies: (object) => [...] }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
const normalizeComponent = (comp) => {
|
|
103
|
+
let componentRef;
|
|
104
|
+
let propsValue;
|
|
105
|
+
let dependenciesFn;
|
|
106
|
+
|
|
107
|
+
// If it's a direct component reference
|
|
108
|
+
if (typeof comp === 'function' || (comp && typeof comp === 'object' && !comp.component)) {
|
|
109
|
+
componentRef = comp;
|
|
110
|
+
propsValue = undefined;
|
|
111
|
+
dependenciesFn = undefined;
|
|
112
|
+
}
|
|
113
|
+
// If it's a configuration object with component and props
|
|
114
|
+
else if (comp && typeof comp === 'object' && comp.component) {
|
|
115
|
+
componentRef = comp.component;
|
|
116
|
+
// Support both "data" (legacy) and "props" (new) for backward compatibility
|
|
117
|
+
propsValue = comp.props !== undefined ? comp.props : comp.data;
|
|
118
|
+
dependenciesFn = comp.dependencies;
|
|
119
|
+
}
|
|
120
|
+
// Fallback: treat as direct component
|
|
121
|
+
else {
|
|
122
|
+
componentRef = comp;
|
|
123
|
+
propsValue = undefined;
|
|
124
|
+
dependenciesFn = undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Return props directly (object or function), not as computed
|
|
128
|
+
// The computed will be created in the template when needed
|
|
129
|
+
return {
|
|
130
|
+
component: componentRef,
|
|
131
|
+
props: typeof propsValue === 'function' ? propsValue(object) : propsValue || {},
|
|
132
|
+
dependencies: dependenciesFn ? dependenciesFn(object) : []
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Normalize an array of sprite components
|
|
138
|
+
*
|
|
139
|
+
* Applies normalization to each component in the array using `normalizeComponent`.
|
|
140
|
+
*
|
|
141
|
+
* @param components - Array of component references or configuration objects
|
|
142
|
+
* @returns Array of normalized component configurations
|
|
143
|
+
*/
|
|
144
|
+
const normalizeComponents = (components) => {
|
|
145
|
+
return components.map((comp) => normalizeComponent(comp));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalized components to render behind sprites
|
|
151
|
+
* Handles both direct component references and configuration objects with optional props and dependencies
|
|
152
|
+
*
|
|
153
|
+
* Supports multiple formats:
|
|
154
|
+
* 1. Direct component: `ShadowComponent`
|
|
155
|
+
* 2. Configuration object: `{ component: LightHalo, props: {...} }`
|
|
156
|
+
* 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
|
|
157
|
+
* 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
|
|
158
|
+
*
|
|
159
|
+
* Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
|
|
160
|
+
* The object is passed to the dependencies function to allow sprite-specific dependency resolution.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* // Direct component
|
|
165
|
+
* componentsBehind: [ShadowComponent]
|
|
166
|
+
*
|
|
167
|
+
* // With static props
|
|
168
|
+
* componentsBehind: [{ component: LightHalo, props: { radius: 30 } }]
|
|
169
|
+
*
|
|
170
|
+
* // With dynamic props and dependencies
|
|
171
|
+
* componentsBehind: [{
|
|
172
|
+
* component: HealthBar,
|
|
173
|
+
* props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
|
|
174
|
+
* dependencies: (object) => [object.hp, object.param.maxHp]
|
|
175
|
+
* }]
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
const normalizedComponentsBehind = computed(() => {
|
|
179
|
+
return normalizeComponents(componentsBehind());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Normalized components to render in front of sprites
|
|
184
|
+
* Handles both direct component references and configuration objects with optional props and dependencies
|
|
185
|
+
*
|
|
186
|
+
* See `normalizedComponentsBehind` for format details.
|
|
187
|
+
* Components with dependencies will only be displayed when all dependencies are resolved.
|
|
188
|
+
*/
|
|
189
|
+
const normalizedComponentsInFront = computed(() => {
|
|
190
|
+
return normalizeComponents(componentsInFront());
|
|
191
|
+
});
|
|
55
192
|
|
|
56
193
|
/**
|
|
57
194
|
* Determine if the camera should follow this sprite
|
|
@@ -294,7 +431,7 @@
|
|
|
294
431
|
mount((element) => {
|
|
295
432
|
hooks.callHooks("client-sprite-onAdd", object).subscribe()
|
|
296
433
|
hooks.callHooks("client-sceneMap-onAddSprite", client.sceneMap, object).subscribe()
|
|
297
|
-
client.setKeyboardControls(element.directives.controls)
|
|
434
|
+
if (isMe()) client.setKeyboardControls(element.directives.controls)
|
|
298
435
|
})
|
|
299
436
|
|
|
300
437
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { inject } from "../../../core/inject";
|
|
2
|
+
import { RpgClientEngine } from "../../../RpgClientEngine";
|
|
3
|
+
import MobileGui from "./mobile.ce";
|
|
4
|
+
import { signal } from "canvasengine";
|
|
5
|
+
|
|
6
|
+
function isMobile() {
|
|
7
|
+
return /Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry/i.test(navigator.userAgent);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const withMobile = () => (
|
|
11
|
+
{
|
|
12
|
+
gui: [
|
|
13
|
+
{
|
|
14
|
+
id: 'mobile-gui',
|
|
15
|
+
component: MobileGui,
|
|
16
|
+
autoDisplay: true,
|
|
17
|
+
dependencies: () => {
|
|
18
|
+
const engine = inject(RpgClientEngine);
|
|
19
|
+
return [signal(isMobile() ||undefined), engine.controlsReady]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
)
|