@rpgjs/server 5.0.0-alpha.14 → 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/index.js +6051 -13463
- package/dist/index.js.map +1 -1
- package/dist/rooms/map.d.ts +50 -1
- package/package.json +3 -2
- package/src/Player/MoveManager.ts +289 -283
- package/src/Player/Player.ts +37 -3
- package/src/rooms/map.ts +249 -35
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/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";
|
|
@@ -12,6 +12,22 @@ import { BehaviorSubject } from "rxjs";
|
|
|
12
12
|
import { COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_PHYSIC, DAMAGE_SKILL } from "../presets";
|
|
13
13
|
import { z } from "zod";
|
|
14
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
|
+
|
|
15
31
|
/**
|
|
16
32
|
* Zod schema for validating map update request body
|
|
17
33
|
*
|
|
@@ -89,31 +105,57 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
89
105
|
constructor() {
|
|
90
106
|
super();
|
|
91
107
|
this.hooks.callHooks("server-map-onStart", this).subscribe();
|
|
92
|
-
this.throttleSync = this.isStandalone ? 0 :
|
|
108
|
+
this.throttleSync = this.isStandalone ? 0 : 50; // Reduced from 100ms to 50ms for better responsiveness
|
|
93
109
|
this.throttleStorage = this.isStandalone ? 0 : 1000;
|
|
110
|
+
this.sessionExpiryTime = 1000 * 60 * 5; //5 minutes
|
|
111
|
+
this.loop();
|
|
94
112
|
}
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
};
|
|
98
149
|
}
|
|
99
150
|
|
|
100
151
|
onJoin(player: RpgPlayer, conn: MockConnection) {
|
|
101
152
|
player.map = this;
|
|
102
153
|
player.context = context;
|
|
103
154
|
player.conn = conn;
|
|
104
|
-
this.physic.addMovableHitbox(player, player.x(), player.y(), player.hitbox().w, player.hitbox().h, {}, {
|
|
105
|
-
enabled: true,
|
|
106
|
-
friction: 0.8,
|
|
107
|
-
minVelocity: 0.5
|
|
108
|
-
});
|
|
109
|
-
this.physic.registerMovementEvents(player.id, () => {
|
|
110
|
-
player.animationName.set('walk')
|
|
111
|
-
}, () => {
|
|
112
|
-
player.animationName.set('stand')
|
|
113
|
-
})
|
|
114
155
|
player._onInit()
|
|
115
156
|
this.dataIsReady$.pipe(
|
|
116
157
|
finalize(() => {
|
|
158
|
+
player.applyFrames()
|
|
117
159
|
this.hooks
|
|
118
160
|
.callHooks("server-player-onJoinMap", player, this)
|
|
119
161
|
.subscribe();
|
|
@@ -122,10 +164,10 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
122
164
|
}
|
|
123
165
|
|
|
124
166
|
onLeave(player: RpgPlayer, conn: MockConnection) {
|
|
125
|
-
this.physic.removeHitbox(player.id)
|
|
126
167
|
this.hooks
|
|
127
168
|
.callHooks("server-player-onLeaveMap", player, this)
|
|
128
169
|
.subscribe();
|
|
170
|
+
player.pendingInputs = [];
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
get hooks() {
|
|
@@ -166,7 +208,8 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
166
208
|
|
|
167
209
|
@Action('action')
|
|
168
210
|
onAction(player: RpgPlayer, action: any) {
|
|
169
|
-
|
|
211
|
+
// Get collisions using the helper method from RpgCommonMap
|
|
212
|
+
const collisions = (this as any).getCollisions(player.id);
|
|
170
213
|
const events: (RpgEvent | undefined)[] = collisions.map(id => this.getEvent(id))
|
|
171
214
|
if (events.length > 0) {
|
|
172
215
|
events.forEach(event => {
|
|
@@ -178,7 +221,19 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
178
221
|
|
|
179
222
|
@Action('move')
|
|
180
223
|
async onInput(player: RpgPlayer, input: any) {
|
|
181
|
-
|
|
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
|
+
}
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
@Request({
|
|
@@ -191,11 +246,11 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
191
246
|
this.globalConfig = map.config
|
|
192
247
|
this.damageFormulas = map.damageFormulas || {};
|
|
193
248
|
this.damageFormulas = {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
249
|
+
damageSkill: DAMAGE_SKILL,
|
|
250
|
+
damagePhysic: DAMAGE_PHYSIC,
|
|
251
|
+
damageCritical: DAMAGE_CRITICAL,
|
|
252
|
+
coefficientElements: COEFFICIENT_ELEMENTS,
|
|
253
|
+
...this.damageFormulas
|
|
199
254
|
}
|
|
200
255
|
await lastValueFrom(this.hooks.callHooks("server-maps-load", this))
|
|
201
256
|
await lastValueFrom(this.hooks.callHooks("server-worldMaps-load", this))
|
|
@@ -203,13 +258,13 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
203
258
|
map.events = map.events ?? []
|
|
204
259
|
|
|
205
260
|
if (map.id) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
268
|
}
|
|
214
269
|
|
|
215
270
|
await lastValueFrom(this.hooks.callHooks("server-map-onBeforeUpdate", map, this))
|
|
@@ -247,7 +302,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
247
302
|
const parts = urlObj.pathname.split('/');
|
|
248
303
|
// ['', 'world', ':id', 'update'] → index 2
|
|
249
304
|
worldId = parts[2] ?? '';
|
|
250
|
-
} catch {}
|
|
305
|
+
} catch { }
|
|
251
306
|
const payload = await request.json();
|
|
252
307
|
|
|
253
308
|
// Normalize input to array of WorldMapConfig
|
|
@@ -272,6 +327,160 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
272
327
|
return { ok: true } as any;
|
|
273
328
|
}
|
|
274
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
|
+
|
|
275
484
|
/**
|
|
276
485
|
* Get a world manager by id (if multiple supported in future)
|
|
277
486
|
*/
|
|
@@ -359,7 +568,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
359
568
|
if (!eventObj.event) {
|
|
360
569
|
// @ts-ignore
|
|
361
570
|
eventObj = {
|
|
362
|
-
event: eventObj
|
|
571
|
+
event: eventObj
|
|
363
572
|
}
|
|
364
573
|
}
|
|
365
574
|
|
|
@@ -381,7 +590,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
381
590
|
// Check if event is a constructor function (class)
|
|
382
591
|
if (typeof event === 'function') {
|
|
383
592
|
eventInstance = new event();
|
|
384
|
-
}
|
|
593
|
+
}
|
|
385
594
|
// Handle event as an object with hooks
|
|
386
595
|
else {
|
|
387
596
|
// Create a new instance extending RpgPlayer with the hooks from the event object
|
|
@@ -397,7 +606,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
397
606
|
|
|
398
607
|
constructor() {
|
|
399
608
|
super();
|
|
400
|
-
|
|
609
|
+
|
|
401
610
|
// Copy hooks from the event object
|
|
402
611
|
const hookObj = event as EventHooks;
|
|
403
612
|
if (hookObj.onInit) this.onInit = hookObj.onInit.bind(this);
|
|
@@ -419,8 +628,9 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
419
628
|
|
|
420
629
|
eventInstance.x.set(x);
|
|
421
630
|
eventInstance.y.set(y);
|
|
631
|
+
eventInstance.applyFrames()
|
|
422
632
|
if (event.name) eventInstance.name.set(event.name);
|
|
423
|
-
|
|
633
|
+
|
|
424
634
|
this.events()[id] = eventInstance;
|
|
425
635
|
|
|
426
636
|
await eventInstance.execMethod('onInit')
|
|
@@ -434,6 +644,10 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
434
644
|
return this.players()[playerId]
|
|
435
645
|
}
|
|
436
646
|
|
|
647
|
+
getPlayers(): RpgPlayer[] {
|
|
648
|
+
return Object.values(this.players())
|
|
649
|
+
}
|
|
650
|
+
|
|
437
651
|
getEvents(): RpgEvent[] {
|
|
438
652
|
return Object.values(this.events())
|
|
439
653
|
}
|
|
@@ -555,7 +769,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
555
769
|
}
|
|
556
770
|
|
|
557
771
|
export interface RpgMap {
|
|
558
|
-
$send: (conn: MockConnection, data: any) => void;
|
|
772
|
+
$send: (conn: MockConnection, data: any) => void;
|
|
559
773
|
$broadcast: (data: any) => void;
|
|
560
774
|
$sessionTransfer: (userOrPublicId: any | string, targetRoomId: string) => void;
|
|
561
775
|
}
|