@rpgjs/client 5.0.0-alpha.2 → 5.0.0-alpha.20
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/AnimationManager.d.ts +8 -0
- package/dist/Game/Map.d.ts +7 -1
- package/dist/Gui/Gui.d.ts +128 -5
- package/dist/RpgClient.d.ts +217 -59
- package/dist/RpgClientEngine.d.ts +345 -6
- package/dist/Sound.d.ts +199 -0
- package/dist/components/animations/index.d.ts +4 -0
- package/dist/components/dynamics/parse-value.d.ts +1 -0
- package/dist/components/gui/index.d.ts +3 -3
- package/dist/components/index.d.ts +3 -1
- package/dist/components/prebuilt/index.d.ts +18 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/index10.js +149 -4
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +21 -7
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +6 -4
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +11 -14
- package/dist/index13.js.map +1 -1
- package/dist/index14.js +8 -40
- package/dist/index14.js.map +1 -1
- package/dist/index15.js +187 -180
- package/dist/index15.js.map +1 -1
- package/dist/index16.js +104 -7
- package/dist/index16.js.map +1 -1
- package/dist/index17.js +82 -372
- package/dist/index17.js.map +1 -1
- package/dist/index18.js +361 -26
- package/dist/index18.js.map +1 -1
- package/dist/index19.js +46 -20
- package/dist/index19.js.map +1 -1
- package/dist/index2.js +683 -32
- package/dist/index2.js.map +1 -1
- package/dist/index20.js +5 -2417
- package/dist/index20.js.map +1 -1
- package/dist/index21.js +383 -97
- package/dist/index21.js.map +1 -1
- package/dist/index22.js +41 -104
- package/dist/index22.js.map +1 -1
- package/dist/index23.js +21 -67
- package/dist/index23.js.map +1 -1
- package/dist/index24.js +2632 -20
- package/dist/index24.js.map +1 -1
- package/dist/index25.js +107 -34
- package/dist/index25.js.map +1 -1
- package/dist/index26.js +69 -3
- package/dist/index26.js.map +1 -1
- package/dist/index27.js +17 -318
- package/dist/index27.js.map +1 -1
- package/dist/index28.js +24 -22
- package/dist/index28.js.map +1 -1
- package/dist/index29.js +92 -8
- package/dist/index29.js.map +1 -1
- package/dist/index3.js +68 -8
- package/dist/index3.js.map +1 -1
- package/dist/index30.js +37 -7
- package/dist/index30.js.map +1 -1
- package/dist/index31.js +18 -168
- package/dist/index31.js.map +1 -1
- package/dist/index32.js +3 -499
- package/dist/index32.js.map +1 -1
- package/dist/index33.js +332 -9
- package/dist/index33.js.map +1 -1
- package/dist/index34.js +24 -4400
- package/dist/index34.js.map +1 -1
- package/dist/index35.js +6 -311
- package/dist/index35.js.map +1 -1
- package/dist/index36.js +8 -88
- package/dist/index36.js.map +1 -1
- package/dist/index37.js +182 -56
- package/dist/index37.js.map +1 -1
- package/dist/index38.js +500 -16
- package/dist/index38.js.map +1 -1
- package/dist/index39.js +10 -18
- package/dist/index39.js.map +1 -1
- package/dist/index4.js +23 -5
- package/dist/index4.js.map +1 -1
- package/dist/index40.js +7 -0
- package/dist/index40.js.map +1 -0
- package/dist/index41.js +3690 -0
- package/dist/index41.js.map +1 -0
- package/dist/index42.js +77 -0
- package/dist/index42.js.map +1 -0
- package/dist/index43.js +6 -0
- package/dist/index43.js.map +1 -0
- package/dist/index44.js +20 -0
- package/dist/index44.js.map +1 -0
- package/dist/index45.js +146 -0
- package/dist/index45.js.map +1 -0
- package/dist/index46.js +12 -0
- package/dist/index46.js.map +1 -0
- package/dist/index47.js +113 -0
- package/dist/index47.js.map +1 -0
- package/dist/index48.js +136 -0
- package/dist/index48.js.map +1 -0
- package/dist/index49.js +137 -0
- package/dist/index49.js.map +1 -0
- package/dist/index5.js +2 -1
- package/dist/index5.js.map +1 -1
- package/dist/index50.js +112 -0
- package/dist/index50.js.map +1 -0
- package/dist/index51.js +141 -0
- package/dist/index51.js.map +1 -0
- package/dist/index52.js +9 -0
- package/dist/index52.js.map +1 -0
- package/dist/index53.js +54 -0
- package/dist/index53.js.map +1 -0
- package/dist/index6.js +1 -1
- package/dist/index6.js.map +1 -1
- package/dist/index7.js +11 -3
- package/dist/index7.js.map +1 -1
- package/dist/index8.js +68 -7
- package/dist/index8.js.map +1 -1
- package/dist/index9.js +230 -15
- package/dist/index9.js.map +1 -1
- package/dist/presets/animation.d.ts +31 -0
- package/dist/presets/faceset.d.ts +30 -0
- package/dist/presets/index.d.ts +103 -0
- package/dist/presets/lpc.d.ts +89 -0
- package/dist/services/loadMap.d.ts +123 -2
- package/dist/services/mmorpg.d.ts +9 -4
- package/dist/services/standalone.d.ts +51 -2
- package/package.json +22 -18
- package/src/Game/{EffectManager.ts → AnimationManager.ts} +3 -2
- package/src/Game/Map.ts +20 -2
- package/src/Game/Object.ts +163 -9
- package/src/Gui/Gui.ts +300 -17
- package/src/RpgClient.ts +222 -58
- package/src/RpgClientEngine.ts +804 -36
- package/src/Sound.ts +253 -0
- package/src/components/{effects → animations}/animation.ce +3 -6
- package/src/components/{effects → animations}/index.ts +1 -1
- package/src/components/character.ce +165 -37
- package/src/components/dynamics/parse-value.ts +80 -0
- package/src/components/dynamics/text.ce +183 -0
- package/src/components/gui/box.ce +17 -0
- package/src/components/gui/dialogbox/index.ce +73 -35
- package/src/components/gui/dialogbox/selection.ce +16 -1
- package/src/components/gui/index.ts +3 -4
- package/src/components/index.ts +5 -1
- package/src/components/prebuilt/hp-bar.ce +255 -0
- package/src/components/prebuilt/index.ts +21 -0
- package/src/components/scenes/draw-map.ce +6 -23
- package/src/components/scenes/event-layer.ce +9 -3
- package/src/core/setup.ts +2 -0
- package/src/index.ts +5 -2
- package/src/module.ts +72 -6
- package/src/presets/animation.ts +46 -0
- package/src/presets/faceset.ts +60 -0
- package/src/presets/index.ts +7 -1
- package/src/presets/lpc.ts +108 -0
- package/src/services/loadMap.ts +132 -3
- package/src/services/mmorpg.ts +27 -5
- package/src/services/standalone.ts +68 -6
- package/tsconfig.json +1 -1
- package/vite.config.ts +1 -1
- package/dist/Game/EffectManager.d.ts +0 -5
- package/dist/components/effects/index.d.ts +0 -4
- package/src/components/scenes/element-map.ce +0 -23
- /package/src/components/{effects → animations}/hit.ce +0 -0
package/dist/index2.js
CHANGED
|
@@ -1,44 +1,103 @@
|
|
|
1
|
-
import component from './
|
|
2
|
-
import { inject } from './
|
|
3
|
-
import { signal, bootstrapCanvas } from 'canvasengine';
|
|
4
|
-
import { WebSocketToken } from './
|
|
1
|
+
import component from './index19.js';
|
|
2
|
+
import { inject } from './index18.js';
|
|
3
|
+
import { signal, bootstrapCanvas, Howl } from 'canvasengine';
|
|
4
|
+
import { WebSocketToken } from './index20.js';
|
|
5
5
|
import { LoadMapToken } from './index7.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { RpgSound } from './index17.js';
|
|
7
|
+
import { Direction, PredictionController, ModulesToken } from '@rpgjs/common';
|
|
8
|
+
import { load } from './index21.js';
|
|
9
|
+
import { RpgClientMap } from './index22.js';
|
|
9
10
|
import { RpgGui } from './index9.js';
|
|
10
|
-
import {
|
|
11
|
+
import { AnimationManager } from './index23.js';
|
|
11
12
|
import { lastValueFrom } from 'rxjs';
|
|
12
13
|
import { GlobalConfigToken } from './index8.js';
|
|
14
|
+
import * as PIXI from 'pixi.js';
|
|
15
|
+
import { PrebuiltComponentAnimations } from './index12.js';
|
|
13
16
|
|
|
14
17
|
class RpgClientEngine {
|
|
15
18
|
constructor(context) {
|
|
16
19
|
this.context = context;
|
|
17
|
-
this.sceneMap = new RpgClientMap();
|
|
18
20
|
this.stopProcessingInput = false;
|
|
19
21
|
this.width = signal("100%");
|
|
20
22
|
this.height = signal("100%");
|
|
21
23
|
this.spritesheets = /* @__PURE__ */ new Map();
|
|
22
24
|
this.sounds = /* @__PURE__ */ new Map();
|
|
23
|
-
this.
|
|
25
|
+
this.componentAnimations = [];
|
|
24
26
|
this.particleSettings = {
|
|
25
27
|
emitters: []
|
|
26
28
|
};
|
|
29
|
+
this.playerIdSignal = signal(null);
|
|
30
|
+
this.spriteComponentsBehind = signal([]);
|
|
31
|
+
this.spriteComponentsInFront = signal([]);
|
|
32
|
+
this.predictionEnabled = false;
|
|
33
|
+
this.SERVER_CORRECTION_THRESHOLD = 30;
|
|
34
|
+
this.inputFrameCounter = 0;
|
|
35
|
+
this.frameOffset = 0;
|
|
36
|
+
// Ping/Pong for RTT measurement
|
|
37
|
+
this.rtt = 0;
|
|
38
|
+
// Round-trip time in ms
|
|
39
|
+
this.pingInterval = null;
|
|
40
|
+
this.PING_INTERVAL_MS = 5e3;
|
|
41
|
+
// Send ping every 5 seconds
|
|
42
|
+
this.lastInputTime = 0;
|
|
27
43
|
this.webSocket = inject(context, WebSocketToken);
|
|
28
44
|
this.guiService = inject(context, RpgGui);
|
|
29
45
|
this.loadMapService = inject(context, LoadMapToken);
|
|
30
46
|
this.hooks = inject(context, ModulesToken);
|
|
31
47
|
this.globalConfig = inject(context, GlobalConfigToken);
|
|
48
|
+
if (!this.globalConfig) {
|
|
49
|
+
this.globalConfig = {};
|
|
50
|
+
}
|
|
51
|
+
if (!this.globalConfig.box) {
|
|
52
|
+
this.globalConfig.box = {
|
|
53
|
+
styles: {
|
|
54
|
+
backgroundColor: "#1a1a2e",
|
|
55
|
+
backgroundOpacity: 0.9
|
|
56
|
+
},
|
|
57
|
+
sounds: {}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
this.addComponentAnimation({
|
|
61
|
+
id: "animation",
|
|
62
|
+
component: PrebuiltComponentAnimations.Animation
|
|
63
|
+
});
|
|
64
|
+
this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
|
|
65
|
+
this.initializePredictionController();
|
|
32
66
|
}
|
|
33
67
|
async start() {
|
|
68
|
+
this.sceneMap = new RpgClientMap();
|
|
34
69
|
this.selector = document.body.querySelector("#rpg");
|
|
35
|
-
await bootstrapCanvas(this.selector, component);
|
|
36
|
-
|
|
70
|
+
const { app, canvasElement } = await bootstrapCanvas(this.selector, component);
|
|
71
|
+
this.renderer = app.renderer;
|
|
72
|
+
this.tick = canvasElement?.propObservables?.context["tick"].observable;
|
|
73
|
+
this.tick.subscribe(() => {
|
|
74
|
+
if (Date.now() - this.lastInputTime > 100) {
|
|
75
|
+
const player = this.getCurrentPlayer();
|
|
76
|
+
if (!player) return;
|
|
77
|
+
this.sceneMap.stopMovement(player);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
37
80
|
this.hooks.callHooks("client-spritesheets-load", this).subscribe();
|
|
81
|
+
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
38
82
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
83
|
+
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
84
|
+
RpgSound.init(this);
|
|
39
85
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
40
86
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
41
|
-
this.hooks.callHooks("client-
|
|
87
|
+
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
88
|
+
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
89
|
+
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
90
|
+
window.addEventListener("resize", () => {
|
|
91
|
+
this.hooks.callHooks("client-engine-onWindowResize", this).subscribe();
|
|
92
|
+
});
|
|
93
|
+
this.tick.subscribe((tick) => {
|
|
94
|
+
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
95
|
+
if (tick % 60 === 0) {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
this.prediction?.cleanup(now);
|
|
98
|
+
this.prediction?.tryApplyPendingSnapshot();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
42
101
|
await this.webSocket.connection(() => {
|
|
43
102
|
this.initListeners();
|
|
44
103
|
this.guiService._initialize();
|
|
@@ -46,21 +105,122 @@ class RpgClientEngine {
|
|
|
46
105
|
}
|
|
47
106
|
initListeners() {
|
|
48
107
|
this.webSocket.on("sync", (data) => {
|
|
108
|
+
if (data.pId) this.playerIdSignal.set(data.pId);
|
|
109
|
+
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
49
110
|
load(this.sceneMap, data, true);
|
|
50
111
|
});
|
|
112
|
+
this.webSocket.on("pong", (data) => {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
this.rtt = now - data.clientTime;
|
|
115
|
+
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1e3 / 60));
|
|
116
|
+
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
117
|
+
if (this.inputFrameCounter > 0) {
|
|
118
|
+
this.frameOffset = estimatedServerTickNow - data.clientFrame;
|
|
119
|
+
}
|
|
120
|
+
console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);
|
|
121
|
+
});
|
|
51
122
|
this.webSocket.on("changeMap", (data) => {
|
|
123
|
+
this.sceneMap.reset();
|
|
52
124
|
this.loadScene(data.mapId);
|
|
53
125
|
});
|
|
54
|
-
this.webSocket.on("
|
|
55
|
-
const { params, object, id } = data;
|
|
56
|
-
if (!object) {
|
|
57
|
-
throw new Error("
|
|
126
|
+
this.webSocket.on("showComponentAnimation", (data) => {
|
|
127
|
+
const { params, object, position, id } = data;
|
|
128
|
+
if (!object && position === void 0) {
|
|
129
|
+
throw new Error("Please provide an object or x and y coordinates");
|
|
58
130
|
}
|
|
131
|
+
const player = object ? this.sceneMap.getObjectById(object) : void 0;
|
|
132
|
+
this.getComponentAnimation(id).displayEffect(params, player || position);
|
|
133
|
+
});
|
|
134
|
+
this.webSocket.on("setAnimation", (data) => {
|
|
135
|
+
const { animationName, nbTimes, object } = data;
|
|
59
136
|
const player = this.sceneMap.getObjectById(object);
|
|
60
|
-
|
|
137
|
+
player.setAnimation(animationName, nbTimes);
|
|
138
|
+
});
|
|
139
|
+
this.webSocket.on("playSound", (data) => {
|
|
140
|
+
const { soundId, volume, loop } = data;
|
|
141
|
+
this.playSound(soundId, { volume, loop });
|
|
142
|
+
});
|
|
143
|
+
this.webSocket.on("stopSound", (data) => {
|
|
144
|
+
const { soundId } = data;
|
|
145
|
+
this.stopSound(soundId);
|
|
146
|
+
});
|
|
147
|
+
this.webSocket.on("open", () => {
|
|
148
|
+
this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
|
|
149
|
+
});
|
|
150
|
+
this.webSocket.on("close", () => {
|
|
151
|
+
this.hooks.callHooks("client-engine-onDisconnected", this, this.socket).subscribe();
|
|
152
|
+
this.stopPingPong();
|
|
153
|
+
});
|
|
154
|
+
this.webSocket.on("error", (error) => {
|
|
155
|
+
this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Start periodic ping/pong for client-server synchronization
|
|
160
|
+
*
|
|
161
|
+
* Sends ping requests to the server to measure round-trip time (RTT) and
|
|
162
|
+
* calculate the frame offset between client and server ticks.
|
|
163
|
+
*
|
|
164
|
+
* ## Design
|
|
165
|
+
*
|
|
166
|
+
* - Sends ping every 5 seconds
|
|
167
|
+
* - Measures RTT for latency compensation
|
|
168
|
+
* - Calculates frame offset to map client frames to server ticks
|
|
169
|
+
* - Used for accurate server reconciliation
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* // Called automatically when connection opens
|
|
174
|
+
* this.startPingPong();
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
startPingPong() {
|
|
178
|
+
this.stopPingPong();
|
|
179
|
+
this.sendPing();
|
|
180
|
+
this.pingInterval = setInterval(() => {
|
|
181
|
+
this.sendPing();
|
|
182
|
+
}, this.PING_INTERVAL_MS);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Stop periodic ping/pong
|
|
186
|
+
*
|
|
187
|
+
* Stops the ping interval when disconnecting or changing maps.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* // Called automatically when connection closes
|
|
192
|
+
* this.stopPingPong();
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
stopPingPong() {
|
|
196
|
+
if (this.pingInterval) {
|
|
197
|
+
clearInterval(this.pingInterval);
|
|
198
|
+
this.pingInterval = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Send a ping request to the server
|
|
203
|
+
*
|
|
204
|
+
* Sends current client time and frame counter to the server,
|
|
205
|
+
* which will respond with its server tick for synchronization.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* // Send a ping to measure RTT
|
|
210
|
+
* this.sendPing();
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
sendPing() {
|
|
214
|
+
const clientTime = Date.now();
|
|
215
|
+
const clientFrame = this.getPhysicsTick();
|
|
216
|
+
this.webSocket.emit("ping", {
|
|
217
|
+
clientTime,
|
|
218
|
+
clientFrame
|
|
61
219
|
});
|
|
62
220
|
}
|
|
63
221
|
async loadScene(mapId) {
|
|
222
|
+
this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap).subscribe();
|
|
223
|
+
this.clearClientPredictionStates();
|
|
64
224
|
this.webSocket.updateProperties({ room: mapId });
|
|
65
225
|
await this.webSocket.reconnect(() => {
|
|
66
226
|
this.initListeners();
|
|
@@ -69,43 +229,534 @@ class RpgClientEngine {
|
|
|
69
229
|
const res = await this.loadMapService.load(mapId);
|
|
70
230
|
this.sceneMap.data.set(res);
|
|
71
231
|
this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap).subscribe();
|
|
232
|
+
this.sceneMap.loadPhysic();
|
|
72
233
|
}
|
|
73
234
|
addSpriteSheet(spritesheetClass, id) {
|
|
74
235
|
this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);
|
|
75
236
|
return spritesheetClass;
|
|
76
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Set a resolver function for spritesheets
|
|
240
|
+
*
|
|
241
|
+
* The resolver is called when a spritesheet is requested but not found in the cache.
|
|
242
|
+
* It can be synchronous (returns directly) or asynchronous (returns a Promise).
|
|
243
|
+
* The resolved spritesheet is automatically cached for future use.
|
|
244
|
+
*
|
|
245
|
+
* @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* // Synchronous resolver
|
|
250
|
+
* engine.setSpritesheetResolver((id) => {
|
|
251
|
+
* if (id === 'dynamic-sprite') {
|
|
252
|
+
* return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };
|
|
253
|
+
* }
|
|
254
|
+
* return undefined;
|
|
255
|
+
* });
|
|
256
|
+
*
|
|
257
|
+
* // Asynchronous resolver (loading from API)
|
|
258
|
+
* engine.setSpritesheetResolver(async (id) => {
|
|
259
|
+
* const response = await fetch(`/api/spritesheets/${id}`);
|
|
260
|
+
* const data = await response.json();
|
|
261
|
+
* return data;
|
|
262
|
+
* });
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
setSpritesheetResolver(resolver) {
|
|
266
|
+
this.spritesheetResolver = resolver;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get a spritesheet by ID, using resolver if not found in cache
|
|
270
|
+
*
|
|
271
|
+
* This method first checks if the spritesheet exists in the cache.
|
|
272
|
+
* If not found and a resolver is set, it calls the resolver to create the spritesheet.
|
|
273
|
+
* The resolved spritesheet is automatically cached for future use.
|
|
274
|
+
*
|
|
275
|
+
* @param id - The spritesheet ID to retrieve
|
|
276
|
+
* @returns The spritesheet if found or created, or undefined if not found and no resolver
|
|
277
|
+
* @returns Promise<any> if the resolver is asynchronous
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```ts
|
|
281
|
+
* // Synchronous usage
|
|
282
|
+
* const spritesheet = engine.getSpriteSheet('my-sprite');
|
|
283
|
+
*
|
|
284
|
+
* // Asynchronous usage (when resolver returns Promise)
|
|
285
|
+
* const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
getSpriteSheet(id) {
|
|
289
|
+
if (this.spritesheets.has(id)) {
|
|
290
|
+
return this.spritesheets.get(id);
|
|
291
|
+
}
|
|
292
|
+
if (this.spritesheetResolver) {
|
|
293
|
+
const result = this.spritesheetResolver(id);
|
|
294
|
+
if (result instanceof Promise) {
|
|
295
|
+
return result.then((spritesheet) => {
|
|
296
|
+
if (spritesheet) {
|
|
297
|
+
this.spritesheets.set(id, spritesheet);
|
|
298
|
+
}
|
|
299
|
+
return spritesheet;
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
if (result) {
|
|
303
|
+
this.spritesheets.set(id, result);
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Add a sound to the engine
|
|
312
|
+
*
|
|
313
|
+
* Adds a sound to the engine's sound cache. The sound can be:
|
|
314
|
+
* - A simple object with `id` and `src` properties
|
|
315
|
+
* - A Howler instance
|
|
316
|
+
* - An object with a `play()` method
|
|
317
|
+
*
|
|
318
|
+
* If the sound has a `src` property, a Howler instance will be created automatically.
|
|
319
|
+
*
|
|
320
|
+
* @param sound - The sound object or Howler instance
|
|
321
|
+
* @param id - Optional sound ID (if not provided, uses sound.id)
|
|
322
|
+
* @returns The added sound
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```ts
|
|
326
|
+
* // Simple sound object
|
|
327
|
+
* engine.addSound({ id: 'click', src: 'click.mp3' });
|
|
328
|
+
*
|
|
329
|
+
* // With explicit ID
|
|
330
|
+
* engine.addSound({ src: 'music.mp3' }, 'background-music');
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
77
333
|
addSound(sound, id) {
|
|
78
|
-
|
|
334
|
+
const soundId = id || sound.id;
|
|
335
|
+
if (!soundId) {
|
|
336
|
+
console.warn("Sound added without an ID. It will not be retrievable.");
|
|
337
|
+
return sound;
|
|
338
|
+
}
|
|
339
|
+
if (sound.src && typeof sound.src === "string") {
|
|
340
|
+
const howlOptions = {
|
|
341
|
+
src: [sound.src],
|
|
342
|
+
loop: sound.loop || false,
|
|
343
|
+
volume: sound.volume !== void 0 ? sound.volume : 1
|
|
344
|
+
};
|
|
345
|
+
const howl = new Howl.Howl(howlOptions);
|
|
346
|
+
this.sounds.set(soundId, howl);
|
|
347
|
+
return howl;
|
|
348
|
+
}
|
|
349
|
+
if (sound && typeof sound.play === "function") {
|
|
350
|
+
this.sounds.set(soundId, sound);
|
|
351
|
+
return sound;
|
|
352
|
+
}
|
|
353
|
+
this.sounds.set(soundId, sound);
|
|
79
354
|
return sound;
|
|
80
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Set a resolver function for sounds
|
|
358
|
+
*
|
|
359
|
+
* The resolver is called when a sound is requested but not found in the cache.
|
|
360
|
+
* It can be synchronous (returns directly) or asynchronous (returns a Promise).
|
|
361
|
+
* The resolved sound is automatically cached for future use.
|
|
362
|
+
*
|
|
363
|
+
* @param resolver - Function that takes a sound ID and returns a sound or Promise of sound
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* // Synchronous resolver
|
|
368
|
+
* engine.setSoundResolver((id) => {
|
|
369
|
+
* if (id === 'dynamic-sound') {
|
|
370
|
+
* return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };
|
|
371
|
+
* }
|
|
372
|
+
* return undefined;
|
|
373
|
+
* });
|
|
374
|
+
*
|
|
375
|
+
* // Asynchronous resolver (loading from API)
|
|
376
|
+
* engine.setSoundResolver(async (id) => {
|
|
377
|
+
* const response = await fetch(`/api/sounds/${id}`);
|
|
378
|
+
* const data = await response.json();
|
|
379
|
+
* return data;
|
|
380
|
+
* });
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
setSoundResolver(resolver) {
|
|
384
|
+
this.soundResolver = resolver;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get a sound by ID, using resolver if not found in cache
|
|
388
|
+
*
|
|
389
|
+
* This method first checks if the sound exists in the cache.
|
|
390
|
+
* If not found and a resolver is set, it calls the resolver to create the sound.
|
|
391
|
+
* The resolved sound is automatically cached for future use.
|
|
392
|
+
*
|
|
393
|
+
* @param id - The sound ID to retrieve
|
|
394
|
+
* @returns The sound if found or created, or undefined if not found and no resolver
|
|
395
|
+
* @returns Promise<any> if the resolver is asynchronous
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* // Synchronous usage
|
|
400
|
+
* const sound = engine.getSound('my-sound');
|
|
401
|
+
*
|
|
402
|
+
* // Asynchronous usage (when resolver returns Promise)
|
|
403
|
+
* const sound = await engine.getSound('dynamic-sound');
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
getSound(id) {
|
|
407
|
+
if (this.sounds.has(id)) {
|
|
408
|
+
return this.sounds.get(id);
|
|
409
|
+
}
|
|
410
|
+
if (this.soundResolver) {
|
|
411
|
+
const result = this.soundResolver(id);
|
|
412
|
+
if (result instanceof Promise) {
|
|
413
|
+
return result.then((sound) => {
|
|
414
|
+
if (sound) {
|
|
415
|
+
this.sounds.set(id, sound);
|
|
416
|
+
}
|
|
417
|
+
return sound;
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
if (result) {
|
|
421
|
+
this.sounds.set(id, result);
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Play a sound by its ID
|
|
430
|
+
*
|
|
431
|
+
* This method retrieves a sound from the cache or resolver and plays it.
|
|
432
|
+
* If the sound is not found, it will attempt to resolve it using the soundResolver.
|
|
433
|
+
* Uses Howler.js for audio playback instead of native Audio elements.
|
|
434
|
+
*
|
|
435
|
+
* @param soundId - The sound ID to play
|
|
436
|
+
* @param options - Optional sound configuration
|
|
437
|
+
* @param options.volume - Volume level (0.0 to 1.0, overrides sound default)
|
|
438
|
+
* @param options.loop - Whether the sound should loop (overrides sound default)
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```ts
|
|
442
|
+
* // Play a sound synchronously
|
|
443
|
+
* engine.playSound('item-pickup');
|
|
444
|
+
*
|
|
445
|
+
* // Play a sound with volume and loop
|
|
446
|
+
* engine.playSound('background-music', { volume: 0.5, loop: true });
|
|
447
|
+
*
|
|
448
|
+
* // Play a sound asynchronously (when resolver returns Promise)
|
|
449
|
+
* await engine.playSound('dynamic-sound', { volume: 0.8 });
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
async playSound(soundId, options) {
|
|
453
|
+
const sound = await this.getSound(soundId);
|
|
454
|
+
if (sound && sound.play) {
|
|
455
|
+
const howlSoundId = sound._sounds?.[0]?._id;
|
|
456
|
+
if (options?.volume !== void 0) {
|
|
457
|
+
if (howlSoundId !== void 0) {
|
|
458
|
+
sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);
|
|
459
|
+
} else {
|
|
460
|
+
sound.volume(Math.max(0, Math.min(1, options.volume)));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (options?.loop !== void 0) {
|
|
464
|
+
if (howlSoundId !== void 0) {
|
|
465
|
+
sound.loop(options.loop, howlSoundId);
|
|
466
|
+
} else {
|
|
467
|
+
sound.loop(options.loop);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (howlSoundId !== void 0) {
|
|
471
|
+
sound.play(howlSoundId);
|
|
472
|
+
} else {
|
|
473
|
+
sound.play();
|
|
474
|
+
}
|
|
475
|
+
} else if (sound && sound.src) {
|
|
476
|
+
const howlOptions = {
|
|
477
|
+
src: [sound.src],
|
|
478
|
+
loop: options?.loop !== void 0 ? options.loop : sound.loop || false,
|
|
479
|
+
volume: options?.volume !== void 0 ? Math.max(0, Math.min(1, options.volume)) : sound.volume !== void 0 ? sound.volume : 1
|
|
480
|
+
};
|
|
481
|
+
const howl = new Howl.Howl(howlOptions);
|
|
482
|
+
this.sounds.set(soundId, howl);
|
|
483
|
+
howl.play();
|
|
484
|
+
} else {
|
|
485
|
+
console.warn(`Sound with id "${soundId}" not found or cannot be played`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Stop a sound that is currently playing
|
|
490
|
+
*
|
|
491
|
+
* This method stops a sound that was previously started with `playSound()`.
|
|
492
|
+
*
|
|
493
|
+
* @param soundId - The sound ID to stop
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* // Start a looping sound
|
|
498
|
+
* engine.playSound('background-music', { loop: true });
|
|
499
|
+
*
|
|
500
|
+
* // Later, stop it
|
|
501
|
+
* engine.stopSound('background-music');
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
stopSound(soundId) {
|
|
505
|
+
const sound = this.sounds.get(soundId);
|
|
506
|
+
if (sound && sound.stop) {
|
|
507
|
+
sound.stop();
|
|
508
|
+
} else {
|
|
509
|
+
console.warn(`Sound with id "${soundId}" not found or cannot be stopped`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
81
512
|
addParticle(particle) {
|
|
82
513
|
this.particleSettings.emitters.push(particle);
|
|
83
514
|
return particle;
|
|
84
515
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Add a component to render behind sprites
|
|
518
|
+
* Components added with this method will be displayed with a lower z-index than the sprite
|
|
519
|
+
*
|
|
520
|
+
* @param component - The component to add behind sprites
|
|
521
|
+
* @returns The added component
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* ```ts
|
|
525
|
+
* // Add a shadow component behind all sprites
|
|
526
|
+
* engine.addSpriteComponentBehind(ShadowComponent);
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
addSpriteComponentBehind(component) {
|
|
530
|
+
this.spriteComponentsBehind.update((components) => [...components, component]);
|
|
531
|
+
return component;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Add a component to render in front of sprites
|
|
535
|
+
* Components added with this method will be displayed with a higher z-index than the sprite
|
|
536
|
+
*
|
|
537
|
+
* @param component - The component to add in front of sprites
|
|
538
|
+
* @returns The added component
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* // Add a health bar component in front of all sprites
|
|
543
|
+
* engine.addSpriteComponentInFront(HealthBarComponent);
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
addSpriteComponentInFront(component) {
|
|
547
|
+
this.spriteComponentsInFront.update((components) => [...components, component]);
|
|
548
|
+
return component;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Add a component animation to the engine
|
|
552
|
+
*
|
|
553
|
+
* Component animations are temporary visual effects that can be displayed
|
|
554
|
+
* on sprites or objects, such as hit indicators, spell effects, or status animations.
|
|
555
|
+
*
|
|
556
|
+
* @param componentAnimation - The component animation configuration
|
|
557
|
+
* @param componentAnimation.id - Unique identifier for the animation
|
|
558
|
+
* @param componentAnimation.component - The component function to render
|
|
559
|
+
* @returns The added component animation configuration
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```ts
|
|
563
|
+
* // Add a hit animation component
|
|
564
|
+
* engine.addComponentAnimation({
|
|
565
|
+
* id: 'hit',
|
|
566
|
+
* component: HitComponent
|
|
567
|
+
* });
|
|
568
|
+
*
|
|
569
|
+
* // Add an explosion effect component
|
|
570
|
+
* engine.addComponentAnimation({
|
|
571
|
+
* id: 'explosion',
|
|
572
|
+
* component: ExplosionComponent
|
|
573
|
+
* });
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
addComponentAnimation(componentAnimation) {
|
|
577
|
+
const instance = new AnimationManager();
|
|
578
|
+
this.componentAnimations.push({
|
|
579
|
+
id: componentAnimation.id,
|
|
580
|
+
component: componentAnimation.component,
|
|
90
581
|
instance,
|
|
91
582
|
current: instance.current
|
|
92
583
|
});
|
|
93
|
-
return
|
|
584
|
+
return componentAnimation;
|
|
94
585
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Get a component animation by its ID
|
|
588
|
+
*
|
|
589
|
+
* Retrieves the EffectManager instance for a specific component animation,
|
|
590
|
+
* which can be used to display the animation on sprites or objects.
|
|
591
|
+
*
|
|
592
|
+
* @param id - The unique identifier of the component animation
|
|
593
|
+
* @returns The EffectManager instance for the animation
|
|
594
|
+
* @throws Error if the component animation is not found
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* // Get the hit animation and display it
|
|
599
|
+
* const hitAnimation = engine.getComponentAnimation('hit');
|
|
600
|
+
* hitAnimation.displayEffect({ text: "Critical!" }, player);
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
getComponentAnimation(id) {
|
|
604
|
+
const componentAnimation = this.componentAnimations.find((componentAnimation2) => componentAnimation2.id === id);
|
|
605
|
+
if (!componentAnimation) {
|
|
606
|
+
throw new Error(`Component animation with id ${id} not found`);
|
|
99
607
|
}
|
|
100
|
-
return
|
|
608
|
+
return componentAnimation.instance;
|
|
101
609
|
}
|
|
102
|
-
processInput({ input }) {
|
|
103
|
-
|
|
610
|
+
async processInput({ input }) {
|
|
611
|
+
const timestamp = Date.now();
|
|
612
|
+
let frame;
|
|
613
|
+
let tick;
|
|
614
|
+
if (this.predictionEnabled && this.prediction) {
|
|
615
|
+
const meta = this.prediction.recordInput(input, timestamp);
|
|
616
|
+
frame = meta.frame;
|
|
617
|
+
tick = meta.tick;
|
|
618
|
+
} else {
|
|
619
|
+
frame = ++this.inputFrameCounter;
|
|
620
|
+
tick = this.getPhysicsTick();
|
|
621
|
+
}
|
|
622
|
+
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
623
|
+
this.webSocket.emit("move", {
|
|
624
|
+
input,
|
|
625
|
+
timestamp,
|
|
626
|
+
frame,
|
|
627
|
+
tick
|
|
628
|
+
});
|
|
629
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer();
|
|
630
|
+
if (currentPlayer) {
|
|
631
|
+
this.sceneMap.moveBody(currentPlayer, input);
|
|
632
|
+
}
|
|
633
|
+
this.lastInputTime = Date.now();
|
|
634
|
+
this.playerIdSignal();
|
|
104
635
|
}
|
|
105
636
|
processAction({ action }) {
|
|
106
637
|
if (this.stopProcessingInput) return;
|
|
638
|
+
this.hooks.callHooks("client-engine-onInput", this, { input: "action", playerId: this.playerId }).subscribe();
|
|
107
639
|
this.webSocket.emit("action", { action });
|
|
108
640
|
}
|
|
641
|
+
get PIXI() {
|
|
642
|
+
return PIXI;
|
|
643
|
+
}
|
|
644
|
+
get socket() {
|
|
645
|
+
return this.webSocket;
|
|
646
|
+
}
|
|
647
|
+
get playerId() {
|
|
648
|
+
return this.playerIdSignal();
|
|
649
|
+
}
|
|
650
|
+
get scene() {
|
|
651
|
+
return this.sceneMap;
|
|
652
|
+
}
|
|
653
|
+
getPhysicsTick() {
|
|
654
|
+
return this.sceneMap?.getTick?.() ?? 0;
|
|
655
|
+
}
|
|
656
|
+
getLocalPlayerState() {
|
|
657
|
+
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
658
|
+
if (!currentPlayer) {
|
|
659
|
+
return { x: 0, y: 0, direction: Direction.Down };
|
|
660
|
+
}
|
|
661
|
+
const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, "top-left");
|
|
662
|
+
const x = topLeft?.x ?? currentPlayer.x();
|
|
663
|
+
const y = topLeft?.y ?? currentPlayer.y();
|
|
664
|
+
const direction = currentPlayer.direction();
|
|
665
|
+
return { x, y, direction };
|
|
666
|
+
}
|
|
667
|
+
applyAuthoritativeState(state) {
|
|
668
|
+
const player = this.sceneMap?.getCurrentPlayer();
|
|
669
|
+
if (!player) return;
|
|
670
|
+
const hitbox = typeof player.hitbox === "function" ? player.hitbox() : player.hitbox;
|
|
671
|
+
const width = hitbox?.w ?? 0;
|
|
672
|
+
const height = hitbox?.h ?? 0;
|
|
673
|
+
const updated = this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height);
|
|
674
|
+
if (!updated) {
|
|
675
|
+
this.sceneMap.setBodyPosition(player.id, state.x, state.y, "top-left");
|
|
676
|
+
}
|
|
677
|
+
player.x.set(Math.round(state.x));
|
|
678
|
+
player.y.set(Math.round(state.y));
|
|
679
|
+
if (state.direction) {
|
|
680
|
+
player.changeDirection(state.direction);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
initializePredictionController() {
|
|
684
|
+
if (!this.predictionEnabled) {
|
|
685
|
+
this.prediction = void 0;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.prediction = new PredictionController({
|
|
689
|
+
correctionThreshold: this.globalConfig?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
690
|
+
historyTtlMs: this.globalConfig?.prediction?.historyTtlMs ?? 2e3,
|
|
691
|
+
getPhysicsTick: () => this.getPhysicsTick(),
|
|
692
|
+
getCurrentState: () => this.getLocalPlayerState(),
|
|
693
|
+
setAuthoritativeState: (state) => this.applyAuthoritativeState(state)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
getCurrentPlayer() {
|
|
697
|
+
return this.sceneMap.getCurrentPlayer();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Clear client prediction states for cleanup
|
|
701
|
+
*
|
|
702
|
+
* Removes old prediction states and input history to prevent memory leaks.
|
|
703
|
+
* Should be called when changing maps or disconnecting.
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```ts
|
|
707
|
+
* // Clear prediction states when changing maps
|
|
708
|
+
* engine.clearClientPredictionStates();
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
clearClientPredictionStates() {
|
|
712
|
+
this.initializePredictionController();
|
|
713
|
+
this.frameOffset = 0;
|
|
714
|
+
this.inputFrameCounter = 0;
|
|
715
|
+
}
|
|
716
|
+
applyServerAck(ack) {
|
|
717
|
+
if (this.predictionEnabled && this.prediction) {
|
|
718
|
+
this.prediction.applyServerAck({
|
|
719
|
+
frame: ack.frame,
|
|
720
|
+
serverTick: ack.serverTick,
|
|
721
|
+
state: typeof ack.x === "number" && typeof ack.y === "number" ? { x: ack.x, y: ack.y, direction: ack.direction } : void 0
|
|
722
|
+
});
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const player = this.getCurrentPlayer();
|
|
729
|
+
const myId = this.playerIdSignal();
|
|
730
|
+
if (!player || !myId) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const hitbox = typeof player.hitbox === "function" ? player.hitbox() : player.hitbox;
|
|
734
|
+
const width = hitbox?.w ?? 0;
|
|
735
|
+
const height = hitbox?.h ?? 0;
|
|
736
|
+
const updated = this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height);
|
|
737
|
+
if (!updated) {
|
|
738
|
+
this.sceneMap.setBodyPosition(myId, ack.x, ack.y, "top-left");
|
|
739
|
+
}
|
|
740
|
+
player.x.set(Math.round(ack.x));
|
|
741
|
+
player.y.set(Math.round(ack.y));
|
|
742
|
+
if (ack.direction) {
|
|
743
|
+
player.changeDirection(ack.direction);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Replay unacknowledged inputs from a given frame to resimulate client prediction
|
|
748
|
+
* after applying server authority at a certain frame.
|
|
749
|
+
*
|
|
750
|
+
* @param startFrame - The last server-acknowledged frame
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```ts
|
|
754
|
+
* // After applying a server correction at frame N
|
|
755
|
+
* this.replayUnackedInputsFromFrame(N);
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
async replayUnackedInputsFromFrame(_startFrame) {
|
|
759
|
+
}
|
|
109
760
|
}
|
|
110
761
|
|
|
111
762
|
export { RpgClientEngine };
|