@rpgjs/server 5.0.0-alpha.13 → 5.0.0-alpha.15
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/Player/MoveManager.d.ts +2 -2
- package/dist/Player/Player.d.ts +20 -2
- package/dist/RpgServer.d.ts +3 -0
- package/dist/index.js +14248 -18460
- package/dist/index.js.map +1 -1
- package/dist/rooms/map.d.ts +52 -0
- package/package.json +10 -8
- package/src/Player/MoveManager.ts +289 -283
- package/src/Player/Player.ts +37 -3
- package/src/RpgServer.ts +4 -0
- package/src/rooms/map.ts +286 -36
package/src/Player/Player.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "./ParameterManager";
|
|
24
24
|
import { WithItemFixture } from "./ItemFixture";
|
|
25
25
|
import { IItemManager, WithItemManager } from "./ItemManager";
|
|
26
|
-
import { lastValueFrom } from "rxjs";
|
|
26
|
+
import { combineLatest, lastValueFrom } from "rxjs";
|
|
27
27
|
import { IEffectManager, WithEffectManager } from "./EffectManager";
|
|
28
28
|
import { AGI, AGI_CURVE, DEX, DEX_CURVE, INT, INT_CURVE, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, STR, STR_CURVE } from "../presets";
|
|
29
29
|
import { IElementManager, WithElementManager } from "./ElementManager";
|
|
@@ -98,6 +98,20 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
98
98
|
context?: Context;
|
|
99
99
|
conn: MockConnection | null = null;
|
|
100
100
|
touchSide: boolean = false; // Protection against map change loops
|
|
101
|
+
/** Last processed client input timestamp for reconciliation */
|
|
102
|
+
lastProcessedInputTs: number = 0;
|
|
103
|
+
/** Last processed client input frame for reconciliation with server tick */
|
|
104
|
+
_lastFramePositions: {
|
|
105
|
+
frame: number;
|
|
106
|
+
position: {
|
|
107
|
+
x: number;
|
|
108
|
+
y: number;
|
|
109
|
+
direction: Direction;
|
|
110
|
+
};
|
|
111
|
+
serverTick?: number; // Server tick at which this position was computed
|
|
112
|
+
} | null = null;
|
|
113
|
+
|
|
114
|
+
frames: { x: number; y: number; ts: number }[] = [];
|
|
101
115
|
|
|
102
116
|
@sync(RpgPlayer) events = signal<RpgEvent[]>([]);
|
|
103
117
|
|
|
@@ -118,6 +132,14 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
118
132
|
(this as any).addParameter(DEX, DEX_CURVE);
|
|
119
133
|
(this as any).addParameter(AGI, AGI_CURVE);
|
|
120
134
|
(this as any).allRecovery();
|
|
135
|
+
|
|
136
|
+
combineLatest([this.x.observable, this.y.observable]).subscribe(([x, y]) => {
|
|
137
|
+
this.frames = [...this.frames, {
|
|
138
|
+
x: x,
|
|
139
|
+
y: y,
|
|
140
|
+
ts: Date.now(),
|
|
141
|
+
}]
|
|
142
|
+
})
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
_onInit() {
|
|
@@ -128,6 +150,11 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
128
150
|
return inject<Hooks>(this.context as any, ModulesToken);
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
applyFrames() {
|
|
154
|
+
this._frames.set(this.frames)
|
|
155
|
+
this.frames = []
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
async execMethod(method: string, methodData: any[] = [], target?: any) {
|
|
132
159
|
let ret: any;
|
|
133
160
|
if (target) {
|
|
@@ -293,8 +320,15 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
293
320
|
|
|
294
321
|
async teleport(positions: { x: number; y: number }) {
|
|
295
322
|
if (!this.map) return false;
|
|
296
|
-
|
|
297
|
-
|
|
323
|
+
if (this.map.physic) {
|
|
324
|
+
// Skip collision check for teleportation (allow teleporting through walls)
|
|
325
|
+
this.map.physic.updateHitbox(this.id, positions.x, positions.y, undefined, undefined, true);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
this.x.set(positions.x)
|
|
329
|
+
this.y.set(positions.y)
|
|
330
|
+
}
|
|
331
|
+
this._frames.set(this.frames)
|
|
298
332
|
}
|
|
299
333
|
|
|
300
334
|
getCurrentMap<T extends RpgMap = RpgMap>(): T | null {
|
package/src/RpgServer.ts
CHANGED
package/src/rooms/map.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action, MockConnection, Request, Room, RoomOnJoin } from "@signe/room";
|
|
2
|
-
import { Hooks, IceMovement, ModulesToken, ProjectileMovement, ProjectileType, RpgCommonMap, ZoneData, Direction } from "@rpgjs/common";
|
|
2
|
+
import { Hooks, IceMovement, ModulesToken, ProjectileMovement, ProjectileType, RpgCommonMap, ZoneData, Direction, RpgCommonPlayer } from "@rpgjs/common";
|
|
3
3
|
import { WorldMapsManager, type WorldMapConfig } from "@rpgjs/common";
|
|
4
4
|
import { RpgPlayer, RpgEvent } from "../Player/Player";
|
|
5
5
|
import { generateShortUUID, sync, type, users } from "@signe/sync";
|
|
@@ -10,6 +10,42 @@ import { finalize, lastValueFrom } from "rxjs";
|
|
|
10
10
|
import { Subject } from "rxjs";
|
|
11
11
|
import { BehaviorSubject } from "rxjs";
|
|
12
12
|
import { COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_PHYSIC, DAMAGE_SKILL } from "../presets";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface for input controls configuration
|
|
17
|
+
*
|
|
18
|
+
* Defines the structure for input validation and anti-cheat controls
|
|
19
|
+
*/
|
|
20
|
+
export interface Controls {
|
|
21
|
+
/** Maximum allowed time delta between inputs in milliseconds */
|
|
22
|
+
maxTimeDelta?: number;
|
|
23
|
+
/** Maximum allowed frame delta between inputs */
|
|
24
|
+
maxFrameDelta?: number;
|
|
25
|
+
/** Minimum time between inputs in milliseconds */
|
|
26
|
+
minTimeBetweenInputs?: number;
|
|
27
|
+
/** Whether to enable anti-cheat validation */
|
|
28
|
+
enableAntiCheat?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Zod schema for validating map update request body
|
|
33
|
+
*
|
|
34
|
+
* This schema ensures that the required fields are present and properly typed
|
|
35
|
+
* when updating a map configuration.
|
|
36
|
+
*/
|
|
37
|
+
const MapUpdateSchema = z.object({
|
|
38
|
+
/** Configuration object for the map (optional) */
|
|
39
|
+
config: z.any().optional(),
|
|
40
|
+
/** Damage formulas configuration (optional) */
|
|
41
|
+
damageFormulas: z.any().optional(),
|
|
42
|
+
/** Unique identifier for the map (required) */
|
|
43
|
+
id: z.string(),
|
|
44
|
+
/** Width of the map in pixels (required) */
|
|
45
|
+
width: z.number(),
|
|
46
|
+
/** Height of the map in pixels (required) */
|
|
47
|
+
height: z.number(),
|
|
48
|
+
});
|
|
13
49
|
|
|
14
50
|
/**
|
|
15
51
|
* Interface representing hook methods available for map events
|
|
@@ -55,8 +91,7 @@ export type EventPosOption = {
|
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
@Room({
|
|
58
|
-
path: "map-{id}"
|
|
59
|
-
throttleSync: 0
|
|
94
|
+
path: "map-{id}"
|
|
60
95
|
})
|
|
61
96
|
export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
62
97
|
@users(RpgPlayer) players = signal({});
|
|
@@ -67,24 +102,60 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
67
102
|
globalConfig: any = {}
|
|
68
103
|
damageFormulas: any = {}
|
|
69
104
|
|
|
70
|
-
|
|
105
|
+
constructor() {
|
|
106
|
+
super();
|
|
107
|
+
this.hooks.callHooks("server-map-onStart", this).subscribe();
|
|
108
|
+
this.throttleSync = this.isStandalone ? 0 : 50; // Reduced from 100ms to 50ms for better responsiveness
|
|
109
|
+
this.throttleStorage = this.isStandalone ? 0 : 1000;
|
|
110
|
+
this.sessionExpiryTime = 1000 * 60 * 5; //5 minutes
|
|
111
|
+
this.loop();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// autoload by @signe/room
|
|
115
|
+
interceptorPacket(player: RpgPlayer, packet: any, conn: MockConnection) {
|
|
116
|
+
let obj: any = {}
|
|
117
|
+
|
|
118
|
+
if (!player) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add timestamp to sync packets for client-side prediction reconciliation
|
|
123
|
+
if (packet && typeof packet === 'object') {
|
|
124
|
+
obj.timestamp = Date.now();
|
|
125
|
+
|
|
126
|
+
// Add ack info: last processed frame and authoritative position
|
|
127
|
+
if (player) {
|
|
128
|
+
const lastFramePositions = player._lastFramePositions;
|
|
129
|
+
obj.ack = {
|
|
130
|
+
frame: lastFramePositions?.frame ?? player.pendingInputs.length,
|
|
131
|
+
x: lastFramePositions?.position?.x ?? player.x(),
|
|
132
|
+
y: lastFramePositions?.position?.y ?? player.y(),
|
|
133
|
+
direction: lastFramePositions?.position?.direction ?? player.direction(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof packet.value == 'string') {
|
|
139
|
+
return packet
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...packet,
|
|
144
|
+
value: {
|
|
145
|
+
...packet.value,
|
|
146
|
+
...obj
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
71
151
|
onJoin(player: RpgPlayer, conn: MockConnection) {
|
|
72
152
|
player.map = this;
|
|
73
153
|
player.context = context;
|
|
74
154
|
player.conn = conn;
|
|
75
|
-
this.physic.addMovableHitbox(player, player.x(), player.y(), player.hitbox().w, player.hitbox().h, {}, {
|
|
76
|
-
enabled: true,
|
|
77
|
-
friction: 0.8,
|
|
78
|
-
minVelocity: 0.5
|
|
79
|
-
});
|
|
80
|
-
this.physic.registerMovementEvents(player.id, () => {
|
|
81
|
-
player.animationName.set('walk')
|
|
82
|
-
}, () => {
|
|
83
|
-
player.animationName.set('stand')
|
|
84
|
-
})
|
|
85
155
|
player._onInit()
|
|
86
156
|
this.dataIsReady$.pipe(
|
|
87
157
|
finalize(() => {
|
|
158
|
+
player.applyFrames()
|
|
88
159
|
this.hooks
|
|
89
160
|
.callHooks("server-player-onJoinMap", player, this)
|
|
90
161
|
.subscribe();
|
|
@@ -92,6 +163,13 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
92
163
|
).subscribe();
|
|
93
164
|
}
|
|
94
165
|
|
|
166
|
+
onLeave(player: RpgPlayer, conn: MockConnection) {
|
|
167
|
+
this.hooks
|
|
168
|
+
.callHooks("server-player-onLeaveMap", player, this)
|
|
169
|
+
.subscribe();
|
|
170
|
+
player.pendingInputs = [];
|
|
171
|
+
}
|
|
172
|
+
|
|
95
173
|
get hooks() {
|
|
96
174
|
return inject<Hooks>(context, ModulesToken);
|
|
97
175
|
}
|
|
@@ -130,7 +208,8 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
130
208
|
|
|
131
209
|
@Action('action')
|
|
132
210
|
onAction(player: RpgPlayer, action: any) {
|
|
133
|
-
|
|
211
|
+
// Get collisions using the helper method from RpgCommonMap
|
|
212
|
+
const collisions = (this as any).getCollisions(player.id);
|
|
134
213
|
const events: (RpgEvent | undefined)[] = collisions.map(id => this.getEvent(id))
|
|
135
214
|
if (events.length > 0) {
|
|
136
215
|
events.forEach(event => {
|
|
@@ -142,24 +221,36 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
142
221
|
|
|
143
222
|
@Action('move')
|
|
144
223
|
async onInput(player: RpgPlayer, input: any) {
|
|
145
|
-
|
|
224
|
+
if (typeof input?.frame === 'number') {
|
|
225
|
+
// Check if we already have this frame to avoid duplicates
|
|
226
|
+
const existingInput = player.pendingInputs.find(pending => pending.frame === input.frame);
|
|
227
|
+
if (existingInput) {
|
|
228
|
+
return; // Skip duplicate frame
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
player.pendingInputs.push({
|
|
232
|
+
input: input.input,
|
|
233
|
+
frame: input.frame,
|
|
234
|
+
timestamp: input.timestamp || Date.now(),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
146
237
|
}
|
|
147
238
|
|
|
148
239
|
@Request({
|
|
149
240
|
path: "/map/update",
|
|
150
|
-
method: "POST"
|
|
151
|
-
})
|
|
241
|
+
method: "POST"
|
|
242
|
+
}, MapUpdateSchema as any)
|
|
152
243
|
async updateMap(request: Request) {
|
|
153
244
|
const map = await request.json()
|
|
154
245
|
this.data.set(map)
|
|
155
246
|
this.globalConfig = map.config
|
|
156
247
|
this.damageFormulas = map.damageFormulas || {};
|
|
157
248
|
this.damageFormulas = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
249
|
+
damageSkill: DAMAGE_SKILL,
|
|
250
|
+
damagePhysic: DAMAGE_PHYSIC,
|
|
251
|
+
damageCritical: DAMAGE_CRITICAL,
|
|
252
|
+
coefficientElements: COEFFICIENT_ELEMENTS,
|
|
253
|
+
...this.damageFormulas
|
|
163
254
|
}
|
|
164
255
|
await lastValueFrom(this.hooks.callHooks("server-maps-load", this))
|
|
165
256
|
await lastValueFrom(this.hooks.callHooks("server-worldMaps-load", this))
|
|
@@ -167,13 +258,13 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
167
258
|
map.events = map.events ?? []
|
|
168
259
|
|
|
169
260
|
if (map.id) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
261
|
+
const mapFound = this.maps.find(m => m.id === map.id)
|
|
262
|
+
if (mapFound?.events) {
|
|
263
|
+
map.events = [
|
|
264
|
+
...mapFound.events,
|
|
265
|
+
...map.events
|
|
266
|
+
]
|
|
267
|
+
}
|
|
177
268
|
}
|
|
178
269
|
|
|
179
270
|
await lastValueFrom(this.hooks.callHooks("server-map-onBeforeUpdate", map, this))
|
|
@@ -211,7 +302,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
211
302
|
const parts = urlObj.pathname.split('/');
|
|
212
303
|
// ['', 'world', ':id', 'update'] → index 2
|
|
213
304
|
worldId = parts[2] ?? '';
|
|
214
|
-
} catch {}
|
|
305
|
+
} catch { }
|
|
215
306
|
const payload = await request.json();
|
|
216
307
|
|
|
217
308
|
// Normalize input to array of WorldMapConfig
|
|
@@ -236,6 +327,160 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
236
327
|
return { ok: true } as any;
|
|
237
328
|
}
|
|
238
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Process pending inputs for a player with anti-cheat validation
|
|
332
|
+
*
|
|
333
|
+
* This method processes all pending inputs for a player while performing
|
|
334
|
+
* anti-cheat validation to prevent time manipulation and frame skipping.
|
|
335
|
+
* It validates the time deltas between inputs and ensures they are within
|
|
336
|
+
* acceptable ranges. After processing, it saves the last frame position
|
|
337
|
+
* for use in packet interception.
|
|
338
|
+
*
|
|
339
|
+
* @param playerId - The ID of the player to process inputs for
|
|
340
|
+
* @param controls - Optional anti-cheat configuration
|
|
341
|
+
* @returns Promise containing the player and processed input strings
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```ts
|
|
345
|
+
* // Process inputs with default anti-cheat settings
|
|
346
|
+
* const result = await map.processInput('player1');
|
|
347
|
+
* console.log('Processed inputs:', result.inputs);
|
|
348
|
+
*
|
|
349
|
+
* // Process inputs with custom anti-cheat configuration
|
|
350
|
+
* const result = await map.processInput('player1', {
|
|
351
|
+
* maxTimeDelta: 100,
|
|
352
|
+
* maxFrameDelta: 5,
|
|
353
|
+
* minTimeBetweenInputs: 16,
|
|
354
|
+
* enableAntiCheat: true
|
|
355
|
+
* });
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
async processInput(playerId: string, controls?: Controls): Promise<{
|
|
359
|
+
player: RpgPlayer,
|
|
360
|
+
inputs: string[]
|
|
361
|
+
}> {
|
|
362
|
+
const player = this.getPlayer(playerId);
|
|
363
|
+
if (!player) {
|
|
364
|
+
throw new Error(`Player ${playerId} not found`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!player.isConnected()) {
|
|
368
|
+
player.pendingInputs = [];
|
|
369
|
+
return {
|
|
370
|
+
player,
|
|
371
|
+
inputs: []
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const processedInputs: string[] = [];
|
|
376
|
+
const defaultControls: Required<Controls> = {
|
|
377
|
+
maxTimeDelta: 1000, // 1 second max between inputs
|
|
378
|
+
maxFrameDelta: 10, // Max 10 frames skipped
|
|
379
|
+
minTimeBetweenInputs: 16, // ~60fps minimum
|
|
380
|
+
enableAntiCheat: false
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const config = { ...defaultControls, ...controls };
|
|
384
|
+
let lastProcessedTime = player.lastProcessedInputTs || 0;
|
|
385
|
+
let lastProcessedFrame = 0;
|
|
386
|
+
|
|
387
|
+
// Sort inputs by frame number to ensure proper order
|
|
388
|
+
player.pendingInputs.sort((a, b) => (a.frame || 0) - (b.frame || 0));
|
|
389
|
+
|
|
390
|
+
let hasProcessedInputs = false;
|
|
391
|
+
|
|
392
|
+
// Process all pending inputs
|
|
393
|
+
while (player.pendingInputs.length > 0) {
|
|
394
|
+
const input = player.pendingInputs.shift();
|
|
395
|
+
|
|
396
|
+
if (!input || typeof input.frame !== 'number') {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Anti-cheat validation
|
|
401
|
+
if (config.enableAntiCheat) {
|
|
402
|
+
// Check frame delta
|
|
403
|
+
if (input.frame > lastProcessedFrame + config.maxFrameDelta) {
|
|
404
|
+
// Reset to last valid frame
|
|
405
|
+
input.frame = lastProcessedFrame + 1;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check time delta if timestamp is available
|
|
409
|
+
if (input.timestamp && lastProcessedTime > 0) {
|
|
410
|
+
const timeDelta = input.timestamp - lastProcessedTime;
|
|
411
|
+
if (timeDelta > config.maxTimeDelta) {
|
|
412
|
+
input.timestamp = lastProcessedTime + config.minTimeBetweenInputs;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check minimum time between inputs
|
|
417
|
+
if (input.timestamp && lastProcessedTime > 0) {
|
|
418
|
+
const timeDelta = input.timestamp - lastProcessedTime;
|
|
419
|
+
if (timeDelta < config.minTimeBetweenInputs) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Skip if frame is too old (more than 10 frames behind)
|
|
426
|
+
if (input.frame < lastProcessedFrame - 10) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Process the input - update velocity based on the latest input
|
|
431
|
+
if (input.input) {
|
|
432
|
+
await this.movePlayer(player, input.input);
|
|
433
|
+
processedInputs.push(input.input);
|
|
434
|
+
hasProcessedInputs = true;
|
|
435
|
+
lastProcessedTime = input.timestamp || Date.now();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update tracking variables
|
|
439
|
+
lastProcessedFrame = input.frame;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Step physics once after processing all inputs for deterministic physics processing
|
|
443
|
+
// This matches the pattern used in the physic example where stepOneTick()
|
|
444
|
+
// is called in the game loop, not after each individual input. We process all inputs
|
|
445
|
+
// first to determine the final velocity, then step once.
|
|
446
|
+
// By processing all inputs before stepping, we avoid multiple steps that cause sliding
|
|
447
|
+
if (hasProcessedInputs) {
|
|
448
|
+
this.forceSingleTick();
|
|
449
|
+
player.lastProcessedInputTs = lastProcessedTime;
|
|
450
|
+
} else {
|
|
451
|
+
const idleTimeout = Math.max(config.minTimeBetweenInputs * 4, 50);
|
|
452
|
+
const lastTs = player.lastProcessedInputTs || 0;
|
|
453
|
+
if (lastTs > 0 && Date.now() - lastTs > idleTimeout) {
|
|
454
|
+
(this as any).stopMovement(player);
|
|
455
|
+
player.lastProcessedInputTs = 0;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Apply frames after all physics steps have been processed
|
|
460
|
+
player.applyFrames()
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
player,
|
|
464
|
+
inputs: processedInputs
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private loop() {
|
|
469
|
+
setInterval(async () => {
|
|
470
|
+
for (const player of this.getPlayers()) {
|
|
471
|
+
if (player.pendingInputs.length > 0) {
|
|
472
|
+
const anyPlayer = player as RpgPlayer;
|
|
473
|
+
if (!anyPlayer._isProcessingInputs) {
|
|
474
|
+
anyPlayer._isProcessingInputs = true;
|
|
475
|
+
await this.processInput(player.id).finally(() => {
|
|
476
|
+
anyPlayer._isProcessingInputs = false;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}, 50); // Increased frequency from 100ms to 50ms for better responsiveness
|
|
482
|
+
}
|
|
483
|
+
|
|
239
484
|
/**
|
|
240
485
|
* Get a world manager by id (if multiple supported in future)
|
|
241
486
|
*/
|
|
@@ -323,7 +568,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
323
568
|
if (!eventObj.event) {
|
|
324
569
|
// @ts-ignore
|
|
325
570
|
eventObj = {
|
|
326
|
-
event: eventObj
|
|
571
|
+
event: eventObj
|
|
327
572
|
}
|
|
328
573
|
}
|
|
329
574
|
|
|
@@ -345,7 +590,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
345
590
|
// Check if event is a constructor function (class)
|
|
346
591
|
if (typeof event === 'function') {
|
|
347
592
|
eventInstance = new event();
|
|
348
|
-
}
|
|
593
|
+
}
|
|
349
594
|
// Handle event as an object with hooks
|
|
350
595
|
else {
|
|
351
596
|
// Create a new instance extending RpgPlayer with the hooks from the event object
|
|
@@ -361,7 +606,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
361
606
|
|
|
362
607
|
constructor() {
|
|
363
608
|
super();
|
|
364
|
-
|
|
609
|
+
|
|
365
610
|
// Copy hooks from the event object
|
|
366
611
|
const hookObj = event as EventHooks;
|
|
367
612
|
if (hookObj.onInit) this.onInit = hookObj.onInit.bind(this);
|
|
@@ -383,8 +628,9 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
383
628
|
|
|
384
629
|
eventInstance.x.set(x);
|
|
385
630
|
eventInstance.y.set(y);
|
|
631
|
+
eventInstance.applyFrames()
|
|
386
632
|
if (event.name) eventInstance.name.set(event.name);
|
|
387
|
-
|
|
633
|
+
|
|
388
634
|
this.events()[id] = eventInstance;
|
|
389
635
|
|
|
390
636
|
await eventInstance.execMethod('onInit')
|
|
@@ -398,6 +644,10 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
398
644
|
return this.players()[playerId]
|
|
399
645
|
}
|
|
400
646
|
|
|
647
|
+
getPlayers(): RpgPlayer[] {
|
|
648
|
+
return Object.values(this.players())
|
|
649
|
+
}
|
|
650
|
+
|
|
401
651
|
getEvents(): RpgEvent[] {
|
|
402
652
|
return Object.values(this.events())
|
|
403
653
|
}
|
|
@@ -519,7 +769,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
519
769
|
}
|
|
520
770
|
|
|
521
771
|
export interface RpgMap {
|
|
522
|
-
$send: (conn: MockConnection, data: any) => void;
|
|
772
|
+
$send: (conn: MockConnection, data: any) => void;
|
|
523
773
|
$broadcast: (data: any) => void;
|
|
524
774
|
$sessionTransfer: (userOrPublicId: any | string, targetRoomId: string) => void;
|
|
525
775
|
}
|