@rpgjs/client 5.0.0-beta.1 → 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 +49 -0
- package/LICENSE +19 -0
- package/dist/Game/AnimationManager.d.ts +1 -1
- package/dist/Game/AnimationManager.js +18 -9
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/AnimationManager.spec.d.ts +1 -0
- package/dist/Game/Event.js.map +1 -1
- package/dist/Game/Map.d.ts +9 -1
- package/dist/Game/Map.js +63 -5
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.d.ts +47 -15
- package/dist/Game/Object.js +82 -38
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Player.js.map +1 -1
- 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/Gui/Gui.d.ts +17 -4
- package/dist/Gui/Gui.js +78 -48
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/Gui/Gui.spec.d.ts +1 -0
- package/dist/Gui/NotificationManager.js.map +1 -1
- package/dist/Resource.js +1 -1
- package/dist/Resource.js.map +1 -1
- package/dist/RpgClient.d.ts +110 -15
- package/dist/RpgClientEngine.d.ts +86 -10
- package/dist/RpgClientEngine.js +306 -49
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/Sound.js.map +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorateMetadata.js +1 -1
- package/dist/components/animations/animation.ce.js +4 -5
- package/dist/components/animations/animation.ce.js.map +1 -1
- package/dist/components/animations/hit.ce.js +19 -25
- package/dist/components/animations/hit.ce.js.map +1 -1
- package/dist/components/animations/index.js +4 -4
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +422 -240
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/dynamics/bar.ce.js +97 -0
- package/dist/components/dynamics/bar.ce.js.map +1 -0
- package/dist/components/dynamics/image.ce.js +24 -0
- package/dist/components/dynamics/image.ce.js.map +1 -0
- package/dist/components/dynamics/parse-value.d.ts +3 -0
- package/dist/components/dynamics/parse-value.js +54 -35
- package/dist/components/dynamics/parse-value.js.map +1 -1
- package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
- package/dist/components/dynamics/shape-utils.d.ts +16 -0
- package/dist/components/dynamics/shape-utils.js +73 -0
- package/dist/components/dynamics/shape-utils.js.map +1 -0
- package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
- package/dist/components/dynamics/shape.ce.js +84 -0
- package/dist/components/dynamics/shape.ce.js.map +1 -0
- package/dist/components/dynamics/text.ce.js +34 -56
- package/dist/components/dynamics/text.ce.js.map +1 -1
- package/dist/components/gui/box.ce.js +6 -8
- package/dist/components/gui/box.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +56 -62
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +42 -65
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/hud/hud.ce.js +21 -30
- package/dist/components/gui/hud/hud.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +112 -165
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +8 -6
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +52 -69
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +75 -92
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js +5 -4
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js +12 -17
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.js +2 -2
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/mobile.ce.js +5 -4
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js +22 -24
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +72 -249
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +90 -127
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +45 -70
- 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-utils.d.ts +67 -0
- package/dist/components/player-components-utils.js +162 -0
- package/dist/components/player-components-utils.js.map +1 -0
- package/dist/components/player-components-utils.spec.d.ts +1 -0
- package/dist/components/player-components.ce.js +189 -0
- package/dist/components/player-components.ce.js.map +1 -0
- package/dist/components/prebuilt/hp-bar.ce.js +42 -44
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
- package/dist/components/prebuilt/light-halo.ce.js +36 -59
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +165 -21
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +25 -32
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js +9 -8
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/core/inject.js +1 -1
- package/dist/core/inject.js.map +1 -1
- package/dist/core/setup.js +1 -1
- package/dist/core/setup.js.map +1 -1
- package/dist/decorators/spritesheet.d.ts +1 -0
- package/dist/decorators/spritesheet.js +11 -0
- package/dist/decorators/spritesheet.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +26 -21
- package/dist/module.js +15 -1
- package/dist/module.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.0.1}/node_modules/@signe/di/dist/index.js +7 -117
- package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js +239 -0
- package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
- package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js +696 -0
- package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js +44 -0
- package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
- package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/index.js +57 -141
- package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
- package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +27 -27
- package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
- package/dist/presets/animation.js.map +1 -1
- package/dist/presets/faceset.js.map +1 -1
- package/dist/presets/icon.js.map +1 -1
- package/dist/presets/index.js.map +1 -1
- package/dist/presets/lpc.js.map +1 -1
- package/dist/presets/rmspritesheet.js.map +1 -1
- package/dist/services/AbstractSocket.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/keyboardControls.js.map +1 -1
- package/dist/services/loadMap.d.ts +6 -0
- package/dist/services/loadMap.js +1 -1
- package/dist/services/loadMap.js.map +1 -1
- 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 +56 -33
- 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/save.js.map +1 -1
- package/dist/services/save.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 +4 -3
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- package/dist/utils/getEntityProp.js +4 -3
- package/dist/utils/getEntityProp.js.map +1 -1
- package/dist/utils/getEntityProp.spec.d.ts +1 -0
- package/dist/utils/readPropValue.d.ts +2 -0
- package/dist/utils/readPropValue.js +13 -0
- package/dist/utils/readPropValue.js.map +1 -0
- package/package.json +13 -14
- package/src/Game/AnimationManager.spec.ts +30 -0
- package/src/Game/AnimationManager.ts +22 -10
- package/src/Game/Map.ts +91 -2
- package/src/Game/Object.ts +148 -69
- package/src/Game/ProjectileManager.spec.ts +338 -0
- package/src/Game/ProjectileManager.ts +324 -0
- package/src/Gui/Gui.spec.ts +273 -0
- package/src/Gui/Gui.ts +105 -50
- package/src/Resource.ts +1 -2
- package/src/RpgClient.ts +125 -17
- package/src/RpgClientEngine.ts +457 -87
- package/src/components/character.ce +441 -32
- package/src/components/dynamics/bar.ce +88 -0
- package/src/components/dynamics/image.ce +21 -0
- package/src/components/dynamics/parse-value.spec.ts +83 -0
- package/src/components/dynamics/parse-value.ts +111 -37
- package/src/components/dynamics/shape-utils.spec.ts +46 -0
- package/src/components/dynamics/shape-utils.ts +61 -0
- package/src/components/dynamics/shape.ce +90 -0
- package/src/components/dynamics/text.ce +35 -149
- package/src/components/gui/dialogbox/index.ce +18 -8
- 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-utils.spec.ts +109 -0
- package/src/components/player-components-utils.ts +205 -0
- package/src/components/player-components.ce +222 -0
- package/src/components/prebuilt/hp-bar.ce +4 -3
- package/src/components/prebuilt/light-halo.ce +2 -2
- package/src/components/scenes/canvas.ce +175 -8
- package/src/components/scenes/draw-map.ce +18 -17
- package/src/components/scenes/event-layer.ce +1 -2
- package/src/core/setup.ts +2 -2
- package/src/decorators/spritesheet.ts +8 -0
- package/src/index.ts +4 -0
- package/src/module.ts +18 -1
- package/src/services/actionInput.spec.ts +101 -0
- package/src/services/actionInput.ts +53 -0
- package/src/services/loadMap.ts +2 -0
- package/src/services/mmorpg-connection.spec.ts +99 -0
- package/src/services/mmorpg-connection.ts +69 -0
- package/src/services/mmorpg.ts +68 -36
- package/src/services/pointerContext.spec.ts +36 -0
- package/src/services/pointerContext.ts +84 -0
- package/src/services/save.spec.ts +127 -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/utils/getEntityProp.spec.ts +96 -0
- package/src/utils/getEntityProp.ts +4 -3
- package/src/utils/readPropValue.ts +16 -0
- package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
- package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
- package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
- package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
- package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
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
|
-
import { signal, bootstrapCanvas, Howl, trigger } from "canvasengine";
|
|
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 } 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";
|
|
@@ -14,13 +15,23 @@ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, swit
|
|
|
14
15
|
import { GlobalConfigToken } from "./module";
|
|
15
16
|
import * as PIXI from "pixi.js";
|
|
16
17
|
import { PrebuiltComponentAnimations } from "./components/animations";
|
|
18
|
+
import TextComponent from "./components/dynamics/text.ce";
|
|
19
|
+
import BarComponent from "./components/dynamics/bar.ce";
|
|
20
|
+
import ShapeComponent from "./components/dynamics/shape.ce";
|
|
21
|
+
import ImageComponent from "./components/dynamics/image.ce";
|
|
17
22
|
import {
|
|
18
23
|
PredictionController,
|
|
19
24
|
type PredictionHistoryEntry,
|
|
20
25
|
type PredictionState,
|
|
26
|
+
type RpgActionInput,
|
|
27
|
+
type RpgActionName,
|
|
21
28
|
} from "@rpgjs/common";
|
|
22
29
|
import { NotificationManager } from "./Gui/NotificationManager";
|
|
23
30
|
import { SaveClientService } from "./services/save";
|
|
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";
|
|
24
35
|
|
|
25
36
|
interface MovementTrajectoryPoint {
|
|
26
37
|
frame: number;
|
|
@@ -32,6 +43,17 @@ interface MovementTrajectoryPoint {
|
|
|
32
43
|
direction?: Direction;
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
47
|
+
start(config?: T): Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type MapShakeOptions = {
|
|
51
|
+
intensity?: number;
|
|
52
|
+
duration?: number;
|
|
53
|
+
frequency?: number;
|
|
54
|
+
direction?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
35
57
|
export class RpgClientEngine<T = any> {
|
|
36
58
|
private guiService: RpgGui;
|
|
37
59
|
private webSocket: AbstractWebsocket;
|
|
@@ -41,13 +63,16 @@ export class RpgClientEngine<T = any> {
|
|
|
41
63
|
private selector: HTMLElement;
|
|
42
64
|
public globalConfig: T;
|
|
43
65
|
public sceneComponent: any;
|
|
66
|
+
public sceneMapComponent: any = BuiltinSceneMap;
|
|
44
67
|
stopProcessingInput = false;
|
|
45
68
|
width = signal("100%");
|
|
46
69
|
height = signal("100%");
|
|
47
|
-
spritesheets: Map<string, any> = new Map();
|
|
70
|
+
spritesheets: Map<string | number, any> = new Map();
|
|
48
71
|
sounds: Map<string, any> = new Map();
|
|
49
72
|
componentAnimations: any[] = [];
|
|
50
|
-
|
|
73
|
+
projectiles: ProjectileManager;
|
|
74
|
+
pointer: ClientPointerContext = createClientPointerContext();
|
|
75
|
+
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
51
76
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
52
77
|
particleSettings: {
|
|
53
78
|
emitters: any[]
|
|
@@ -61,10 +86,11 @@ export class RpgClientEngine<T = any> {
|
|
|
61
86
|
playerIdSignal = signal<string | null>(null);
|
|
62
87
|
spriteComponentsBehind = signal<any[]>([]);
|
|
63
88
|
spriteComponentsInFront = signal<any[]>([]);
|
|
89
|
+
spriteComponents: Map<string, any> = new Map();
|
|
64
90
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
65
91
|
cameraFollowTargetId = signal<string | null>(null);
|
|
66
92
|
/** Trigger for map shake animation */
|
|
67
|
-
mapShakeTrigger = trigger();
|
|
93
|
+
mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
|
|
68
94
|
|
|
69
95
|
controlsReady = signal(undefined);
|
|
70
96
|
gamePause = signal(false);
|
|
@@ -76,6 +102,8 @@ export class RpgClientEngine<T = any> {
|
|
|
76
102
|
private pendingPredictionFrames: number[] = [];
|
|
77
103
|
private lastClientPhysicsStepAt = 0;
|
|
78
104
|
private frameOffset = 0;
|
|
105
|
+
private latestServerTick?: number;
|
|
106
|
+
private latestServerTickAt = 0;
|
|
79
107
|
// Ping/Pong for RTT measurement
|
|
80
108
|
private rtt: number = 0; // Round-trip time in ms
|
|
81
109
|
private pingInterval: any = null;
|
|
@@ -96,6 +124,9 @@ export class RpgClientEngine<T = any> {
|
|
|
96
124
|
// Store subscriptions and event listeners for cleanup
|
|
97
125
|
private tickSubscriptions: any[] = [];
|
|
98
126
|
private resizeHandler?: () => void;
|
|
127
|
+
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
128
|
+
private pointerCanvas?: HTMLCanvasElement;
|
|
129
|
+
private pendingSyncPackets: any[] = [];
|
|
99
130
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
100
131
|
|
|
101
132
|
constructor(public context) {
|
|
@@ -103,6 +134,10 @@ export class RpgClientEngine<T = any> {
|
|
|
103
134
|
this.guiService = inject(RpgGui);
|
|
104
135
|
this.loadMapService = inject(LoadMapToken);
|
|
105
136
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
137
|
+
this.projectiles = new ProjectileManager(
|
|
138
|
+
this.hooks,
|
|
139
|
+
(projectile) => this.predictProjectileImpact(projectile),
|
|
140
|
+
);
|
|
106
141
|
this.globalConfig = inject(GlobalConfigToken)
|
|
107
142
|
|
|
108
143
|
if (!this.globalConfig) {
|
|
@@ -123,6 +158,13 @@ export class RpgClientEngine<T = any> {
|
|
|
123
158
|
component: PrebuiltComponentAnimations.Animation
|
|
124
159
|
})
|
|
125
160
|
|
|
161
|
+
this.registerSpriteComponent("rpg:text", TextComponent);
|
|
162
|
+
this.registerSpriteComponent("rpg:hpBar", BarComponent);
|
|
163
|
+
this.registerSpriteComponent("rpg:spBar", BarComponent);
|
|
164
|
+
this.registerSpriteComponent("rpg:bar", BarComponent);
|
|
165
|
+
this.registerSpriteComponent("rpg:shape", ShapeComponent);
|
|
166
|
+
this.registerSpriteComponent("rpg:image", ImageComponent);
|
|
167
|
+
|
|
126
168
|
this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
|
|
127
169
|
this.initializePredictionController();
|
|
128
170
|
}
|
|
@@ -173,6 +215,22 @@ export class RpgClientEngine<T = any> {
|
|
|
173
215
|
this.sceneMap = new RpgClientMap()
|
|
174
216
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
175
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
|
+
|
|
176
234
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
177
235
|
|
|
178
236
|
const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
|
|
@@ -183,8 +241,10 @@ export class RpgClientEngine<T = any> {
|
|
|
183
241
|
);
|
|
184
242
|
this.canvasApp = app;
|
|
185
243
|
this.canvasElement = canvasElement;
|
|
186
|
-
this.renderer = app.renderer as PIXI.Renderer;
|
|
244
|
+
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
245
|
+
this.setupPointerTracking();
|
|
187
246
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
247
|
+
this.flushPendingSyncPackets();
|
|
188
248
|
|
|
189
249
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
190
250
|
if (Date.now() - this.lastInputTime > 100) {
|
|
@@ -200,12 +260,13 @@ export class RpgClientEngine<T = any> {
|
|
|
200
260
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
201
261
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
202
262
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
203
|
-
|
|
263
|
+
|
|
204
264
|
RpgSound.init(this);
|
|
205
265
|
RpgResource.init(this);
|
|
206
266
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
207
267
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
208
268
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
269
|
+
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
209
270
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
210
271
|
|
|
211
272
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
@@ -218,6 +279,7 @@ export class RpgClientEngine<T = any> {
|
|
|
218
279
|
|
|
219
280
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
220
281
|
this.stepClientPhysicsTick();
|
|
282
|
+
this.projectiles.step();
|
|
221
283
|
this.flushPendingPredictedStates();
|
|
222
284
|
this.flushPendingMovePath();
|
|
223
285
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
@@ -231,13 +293,56 @@ export class RpgClientEngine<T = any> {
|
|
|
231
293
|
});
|
|
232
294
|
this.tickSubscriptions.push(tickSubscription);
|
|
233
295
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
));
|
|
241
346
|
}
|
|
242
347
|
|
|
243
348
|
private prepareSyncPayload(data: any): any {
|
|
@@ -288,51 +393,11 @@ export class RpgClientEngine<T = any> {
|
|
|
288
393
|
|
|
289
394
|
private initListeners() {
|
|
290
395
|
this.webSocket.on("sync", (data) => {
|
|
291
|
-
if (
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
this.playerIdReceived$.next(true);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (this.sceneResetQueued) {
|
|
298
|
-
this.sceneMap.reset();
|
|
299
|
-
this.sceneMap.loadPhysic();
|
|
300
|
-
this.sceneResetQueued = false;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Apply client-side prediction filtering and server reconciliation
|
|
304
|
-
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
305
|
-
|
|
306
|
-
const ack = data?.ack;
|
|
307
|
-
const normalizedAck =
|
|
308
|
-
ack && typeof ack.frame === "number"
|
|
309
|
-
? this.normalizeAckWithSyncState(ack, data)
|
|
310
|
-
: undefined;
|
|
311
|
-
const payload = this.prepareSyncPayload(data);
|
|
312
|
-
load(this.sceneMap, payload, true);
|
|
313
|
-
|
|
314
|
-
if (normalizedAck) {
|
|
315
|
-
this.applyServerAck(normalizedAck);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
for (const playerId in payload.players ?? {}) {
|
|
319
|
-
const player = payload.players[playerId]
|
|
320
|
-
if (!player._param) continue
|
|
321
|
-
for (const param in player._param) {
|
|
322
|
-
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Check if players and events are present in sync data
|
|
327
|
-
const players = payload.players || this.sceneMap.players();
|
|
328
|
-
if (players && Object.keys(players).length > 0) {
|
|
329
|
-
this.playersReceived$.next(true);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const events = payload.events || this.sceneMap.events();
|
|
333
|
-
if (events !== undefined) {
|
|
334
|
-
this.eventsReceived$.next(true);
|
|
396
|
+
if (!this.tick) {
|
|
397
|
+
this.pendingSyncPackets.push(data);
|
|
398
|
+
return;
|
|
335
399
|
}
|
|
400
|
+
this.applySyncPacket(data);
|
|
336
401
|
});
|
|
337
402
|
|
|
338
403
|
// Handle pong responses for RTT measurement
|
|
@@ -344,6 +409,7 @@ export class RpgClientEngine<T = any> {
|
|
|
344
409
|
// This helps us estimate which server tick corresponds to each client input frame
|
|
345
410
|
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
|
|
346
411
|
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
412
|
+
this.updateServerTickEstimate(estimatedServerTickNow, now);
|
|
347
413
|
|
|
348
414
|
// Update frame offset (only if we have inputs to calibrate with)
|
|
349
415
|
if (this.inputFrameCounter > 0) {
|
|
@@ -355,6 +421,10 @@ export class RpgClientEngine<T = any> {
|
|
|
355
421
|
|
|
356
422
|
this.webSocket.on("changeMap", (data) => {
|
|
357
423
|
this.sceneResetQueued = true;
|
|
424
|
+
this.sceneMap.weatherState.set(null);
|
|
425
|
+
this.sceneMap.lightingState.set(null);
|
|
426
|
+
this.sceneMap.clearLightSpots();
|
|
427
|
+
this.projectiles.clear();
|
|
358
428
|
// Reset camera follow to default (follow current player) when changing maps
|
|
359
429
|
this.cameraFollowTargetId.set(null);
|
|
360
430
|
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
@@ -370,19 +440,50 @@ export class RpgClientEngine<T = any> {
|
|
|
370
440
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
371
441
|
});
|
|
372
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
|
+
|
|
373
462
|
this.webSocket.on("notification", (data) => {
|
|
374
463
|
this.notificationManager.add(data);
|
|
375
464
|
});
|
|
376
465
|
|
|
377
|
-
|
|
378
|
-
const {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
466
|
+
this.webSocket.on("setAnimation", (data) => {
|
|
467
|
+
const {
|
|
468
|
+
animationName,
|
|
469
|
+
nbTimes,
|
|
470
|
+
object,
|
|
471
|
+
graphic,
|
|
472
|
+
restoreAnimationName,
|
|
473
|
+
restoreGraphics,
|
|
474
|
+
} = data;
|
|
475
|
+
const player = object ? this.sceneMap.getObjectById(object) : undefined;
|
|
476
|
+
if (!player) return;
|
|
477
|
+
const restoreOptions = {
|
|
478
|
+
restoreAnimationName,
|
|
479
|
+
restoreGraphics,
|
|
480
|
+
};
|
|
481
|
+
if (graphic !== undefined) {
|
|
482
|
+
player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
|
|
483
|
+
} else {
|
|
484
|
+
player.setAnimation(animationName, nbTimes, restoreOptions);
|
|
485
|
+
}
|
|
486
|
+
})
|
|
386
487
|
|
|
387
488
|
this.webSocket.on("playSound", (data) => {
|
|
388
489
|
const { soundId, volume, loop } = data;
|
|
@@ -413,7 +514,7 @@ export class RpgClientEngine<T = any> {
|
|
|
413
514
|
|
|
414
515
|
this.webSocket.on("shakeMap", (data) => {
|
|
415
516
|
const { intensity, duration, frequency, direction } = data || {};
|
|
416
|
-
|
|
517
|
+
this.mapShakeTrigger.start({
|
|
417
518
|
intensity,
|
|
418
519
|
duration,
|
|
419
520
|
frequency,
|
|
@@ -447,6 +548,14 @@ export class RpgClientEngine<T = any> {
|
|
|
447
548
|
});
|
|
448
549
|
});
|
|
449
550
|
|
|
551
|
+
this.webSocket.on("lightingState", (data) => {
|
|
552
|
+
const raw = (data && typeof data === "object" && "value" in data)
|
|
553
|
+
? (data as any).value
|
|
554
|
+
: data;
|
|
555
|
+
|
|
556
|
+
this.sceneMap.lightingState.set(normalizeLightingState(raw));
|
|
557
|
+
});
|
|
558
|
+
|
|
450
559
|
this.webSocket.on('open', () => {
|
|
451
560
|
this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
|
|
452
561
|
// Start ping/pong for synchronization
|
|
@@ -460,10 +569,72 @@ export class RpgClientEngine<T = any> {
|
|
|
460
569
|
})
|
|
461
570
|
|
|
462
571
|
this.webSocket.on('error', (error) => {
|
|
463
|
-
this.
|
|
572
|
+
void this.callConnectError(error);
|
|
464
573
|
})
|
|
465
574
|
}
|
|
466
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
|
+
|
|
467
638
|
/**
|
|
468
639
|
* Start periodic ping/pong for client-server synchronization
|
|
469
640
|
*
|
|
@@ -560,12 +731,19 @@ export class RpgClientEngine<T = any> {
|
|
|
560
731
|
room: mapId,
|
|
561
732
|
query: transferToken ? { transferToken } : undefined,
|
|
562
733
|
})
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|
|
569
747
|
const res = await this.loadMapService.load(mapId)
|
|
570
748
|
this.sceneMap.data.set(res)
|
|
571
749
|
|
|
@@ -623,7 +801,7 @@ export class RpgClientEngine<T = any> {
|
|
|
623
801
|
* });
|
|
624
802
|
* ```
|
|
625
803
|
*/
|
|
626
|
-
setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
|
|
804
|
+
setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
|
|
627
805
|
this.spritesheetResolver = resolver;
|
|
628
806
|
}
|
|
629
807
|
|
|
@@ -634,7 +812,7 @@ export class RpgClientEngine<T = any> {
|
|
|
634
812
|
* If not found and a resolver is set, it calls the resolver to create the spritesheet.
|
|
635
813
|
* The resolved spritesheet is automatically cached for future use.
|
|
636
814
|
*
|
|
637
|
-
* @param id - The spritesheet ID to retrieve
|
|
815
|
+
* @param id - The spritesheet ID or legacy tile ID to retrieve
|
|
638
816
|
* @returns The spritesheet if found or created, or undefined if not found and no resolver
|
|
639
817
|
* @returns Promise<any> if the resolver is asynchronous
|
|
640
818
|
*
|
|
@@ -647,7 +825,7 @@ export class RpgClientEngine<T = any> {
|
|
|
647
825
|
* const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
|
|
648
826
|
* ```
|
|
649
827
|
*/
|
|
650
|
-
getSpriteSheet(id: string): any | Promise<any> {
|
|
828
|
+
getSpriteSheet(id: string | number): any | Promise<any> {
|
|
651
829
|
// Check cache first
|
|
652
830
|
if (this.spritesheets.has(id)) {
|
|
653
831
|
return this.spritesheets.get(id);
|
|
@@ -1081,6 +1259,45 @@ export class RpgClientEngine<T = any> {
|
|
|
1081
1259
|
return component
|
|
1082
1260
|
}
|
|
1083
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* Register a reusable sprite component that can be addressed by the server.
|
|
1264
|
+
*
|
|
1265
|
+
* Server-side component definitions only carry the component id and
|
|
1266
|
+
* serializable props. The client registry maps that id to the CanvasEngine
|
|
1267
|
+
* component that performs the actual rendering.
|
|
1268
|
+
*
|
|
1269
|
+
* @param id - Stable component id used by server component definitions
|
|
1270
|
+
* @param component - CanvasEngine component to render for this id
|
|
1271
|
+
* @returns The registered component
|
|
1272
|
+
*
|
|
1273
|
+
* @example
|
|
1274
|
+
* ```ts
|
|
1275
|
+
* engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
|
|
1276
|
+
* ```
|
|
1277
|
+
*/
|
|
1278
|
+
registerSpriteComponent(id: string, component: any) {
|
|
1279
|
+
this.spriteComponents.set(id, component);
|
|
1280
|
+
return component;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Get a reusable sprite component by id.
|
|
1285
|
+
*
|
|
1286
|
+
* @param id - Component id registered on the client
|
|
1287
|
+
* @returns The CanvasEngine component, or undefined when missing
|
|
1288
|
+
*/
|
|
1289
|
+
getSpriteComponent(id: string) {
|
|
1290
|
+
return this.spriteComponents.get(id);
|
|
1291
|
+
}
|
|
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
|
+
|
|
1084
1301
|
/**
|
|
1085
1302
|
* Add a component animation to the engine
|
|
1086
1303
|
*
|
|
@@ -1164,16 +1381,43 @@ export class RpgClientEngine<T = any> {
|
|
|
1164
1381
|
* duration: 1000,
|
|
1165
1382
|
* onFinish: () => console.log('Fade complete')
|
|
1166
1383
|
* });
|
|
1384
|
+
*
|
|
1385
|
+
* // Wait until the transition component calls onFinish
|
|
1386
|
+
* await engine.startTransition('fade', { duration: 1000 });
|
|
1167
1387
|
* ```
|
|
1168
1388
|
*/
|
|
1169
|
-
startTransition(id: string, props: any = {}) {
|
|
1389
|
+
startTransition(id: string, props: any = {}): Promise<void> {
|
|
1170
1390
|
if (!this.guiService.exists(id)) {
|
|
1171
1391
|
throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
|
|
1172
1392
|
}
|
|
1173
|
-
|
|
1393
|
+
return new Promise<void>((resolve) => {
|
|
1394
|
+
let finished = false;
|
|
1395
|
+
const finish = (data?: any) => {
|
|
1396
|
+
if (finished) return;
|
|
1397
|
+
finished = true;
|
|
1398
|
+
props?.onFinish?.(data);
|
|
1399
|
+
resolve();
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
this.guiService.display(id, {
|
|
1403
|
+
...props,
|
|
1404
|
+
onFinish: finish,
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1174
1407
|
}
|
|
1175
1408
|
|
|
1176
1409
|
async processInput({ input }: { input: Direction }) {
|
|
1410
|
+
if (this.stopProcessingInput) return;
|
|
1411
|
+
|
|
1412
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1413
|
+
const canMove =
|
|
1414
|
+
!currentPlayer ||
|
|
1415
|
+
getCanMoveValue(currentPlayer);
|
|
1416
|
+
if (!canMove) {
|
|
1417
|
+
this.interruptCurrentPlayerMovement(currentPlayer);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1177
1421
|
const timestamp = Date.now();
|
|
1178
1422
|
let frame: number;
|
|
1179
1423
|
let tick: number;
|
|
@@ -1188,7 +1432,6 @@ export class RpgClientEngine<T = any> {
|
|
|
1188
1432
|
this.inputFrameCounter = frame;
|
|
1189
1433
|
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
1190
1434
|
|
|
1191
|
-
const currentPlayer = this.sceneMap.getCurrentPlayer();
|
|
1192
1435
|
const bodyReady = this.ensureCurrentPlayerBody();
|
|
1193
1436
|
if (currentPlayer && bodyReady) {
|
|
1194
1437
|
currentPlayer.changeDirection(input);
|
|
@@ -1205,10 +1448,25 @@ export class RpgClientEngine<T = any> {
|
|
|
1205
1448
|
this.lastInputTime = Date.now();
|
|
1206
1449
|
}
|
|
1207
1450
|
|
|
1208
|
-
processAction(
|
|
1451
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
1452
|
+
processAction(action: RpgActionInput): void;
|
|
1453
|
+
processAction(action: RpgActionName | RpgActionInput, data?: any): void {
|
|
1209
1454
|
if (this.stopProcessingInput) return;
|
|
1210
|
-
|
|
1211
|
-
|
|
1455
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1456
|
+
const canMove =
|
|
1457
|
+
!currentPlayer ||
|
|
1458
|
+
getCanMoveValue(currentPlayer);
|
|
1459
|
+
if (!canMove) return;
|
|
1460
|
+
|
|
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)
|
|
1212
1470
|
}
|
|
1213
1471
|
|
|
1214
1472
|
get PIXI() {
|
|
@@ -1228,7 +1486,71 @@ export class RpgClientEngine<T = any> {
|
|
|
1228
1486
|
}
|
|
1229
1487
|
|
|
1230
1488
|
private getPhysicsTick(): number {
|
|
1231
|
-
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
|
+
};
|
|
1232
1554
|
}
|
|
1233
1555
|
|
|
1234
1556
|
private ensureCurrentPlayerBody(): boolean {
|
|
@@ -1337,6 +1659,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1337
1659
|
if (!this.predictionEnabled || !this.prediction) {
|
|
1338
1660
|
return;
|
|
1339
1661
|
}
|
|
1662
|
+
const player = this.sceneMap?.getCurrentPlayer?.() as any;
|
|
1663
|
+
if (
|
|
1664
|
+
player &&
|
|
1665
|
+
!getCanMoveValue(player)
|
|
1666
|
+
) {
|
|
1667
|
+
this.interruptCurrentPlayerMovement(player);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1340
1670
|
const pendingInputs = this.prediction.getPendingInputs();
|
|
1341
1671
|
if (pendingInputs.length === 0) {
|
|
1342
1672
|
return;
|
|
@@ -1475,6 +1805,34 @@ export class RpgClientEngine<T = any> {
|
|
|
1475
1805
|
this.lastMovePathSentFrame = 0;
|
|
1476
1806
|
}
|
|
1477
1807
|
|
|
1808
|
+
/**
|
|
1809
|
+
* Stop local movement immediately and discard pending predicted movement.
|
|
1810
|
+
*
|
|
1811
|
+
* Use this before a blocking action such as an A-RPG attack, dialog, dash
|
|
1812
|
+
* startup, or any client-side state where already buffered movement inputs
|
|
1813
|
+
* must not be replayed after server reconciliation.
|
|
1814
|
+
*
|
|
1815
|
+
* @param player - Player object to stop. Defaults to the current player.
|
|
1816
|
+
* @returns `true` when a player was found and interrupted.
|
|
1817
|
+
*
|
|
1818
|
+
* @example
|
|
1819
|
+
* ```ts
|
|
1820
|
+
* engine.interruptCurrentPlayerMovement();
|
|
1821
|
+
* ```
|
|
1822
|
+
*/
|
|
1823
|
+
interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
|
|
1824
|
+
if (!player) {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
(this.sceneMap as any)?.stopMovement?.(player);
|
|
1828
|
+
this.prediction?.clearPendingInputs();
|
|
1829
|
+
this.pendingPredictionFrames = [];
|
|
1830
|
+
this.lastInputTime = 0;
|
|
1831
|
+
this.lastMovePathSentAt = Date.now();
|
|
1832
|
+
this.lastMovePathSentFrame = this.inputFrameCounter;
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1478
1836
|
/**
|
|
1479
1837
|
* Trigger a flash animation on a sprite
|
|
1480
1838
|
*
|
|
@@ -1542,6 +1900,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1542
1900
|
}
|
|
1543
1901
|
|
|
1544
1902
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
1903
|
+
this.updateServerTickEstimate(ack.serverTick);
|
|
1545
1904
|
if (this.predictionEnabled && this.prediction) {
|
|
1546
1905
|
const result = this.prediction.applyServerAck({
|
|
1547
1906
|
frame: ack.frame,
|
|
@@ -1560,7 +1919,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1560
1919
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
1561
1920
|
return;
|
|
1562
1921
|
}
|
|
1563
|
-
const player = this.getCurrentPlayer();
|
|
1922
|
+
const player = this.getCurrentPlayer() as any;
|
|
1564
1923
|
const myId = this.playerIdSignal();
|
|
1565
1924
|
if (!player || !myId) {
|
|
1566
1925
|
return;
|
|
@@ -1583,10 +1942,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1583
1942
|
authoritativeState: PredictionState<Direction>,
|
|
1584
1943
|
pendingInputs: PredictionHistoryEntry<Direction>[],
|
|
1585
1944
|
): void {
|
|
1586
|
-
const player = this.getCurrentPlayer();
|
|
1945
|
+
const player = this.getCurrentPlayer() as any;
|
|
1587
1946
|
if (!player) {
|
|
1588
1947
|
return;
|
|
1589
1948
|
}
|
|
1949
|
+
if (!getCanMoveValue(player)) {
|
|
1950
|
+
this.interruptCurrentPlayerMovement(player);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1590
1953
|
|
|
1591
1954
|
(this.sceneMap as any).stopMovement(player);
|
|
1592
1955
|
this.applyAuthoritativeState(authoritativeState);
|
|
@@ -1699,6 +2062,13 @@ export class RpgClientEngine<T = any> {
|
|
|
1699
2062
|
this.resizeHandler = undefined;
|
|
1700
2063
|
}
|
|
1701
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
|
+
|
|
1702
2072
|
// Destroy PIXI app and renderer if they exist
|
|
1703
2073
|
// Destroy the app first, which will destroy the renderer
|
|
1704
2074
|
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|