@rpgjs/server 5.0.0-alpha.24 → 5.0.0-alpha.26
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/BattleManager.d.ts +1 -1
- package/dist/Player/ClassManager.d.ts +1 -1
- package/dist/Player/ComponentManager.d.ts +1 -1
- package/dist/Player/EffectManager.d.ts +1 -1
- package/dist/Player/ElementManager.d.ts +1 -1
- package/dist/Player/GoldManager.d.ts +1 -1
- package/dist/Player/GuiManager.d.ts +1 -1
- package/dist/Player/ItemFixture.d.ts +1 -1
- package/dist/Player/ItemManager.d.ts +1 -1
- package/dist/Player/MoveManager.d.ts +1 -1
- package/dist/Player/ParameterManager.d.ts +1 -1
- package/dist/Player/Player.d.ts +32 -23
- package/dist/Player/SkillManager.d.ts +1 -1
- package/dist/Player/StateManager.d.ts +1 -1
- package/dist/Player/VariableManager.d.ts +1 -1
- package/dist/RpgServer.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8267 -10267
- package/dist/index.js.map +1 -1
- package/dist/rooms/BaseRoom.d.ts +95 -0
- package/dist/rooms/lobby.d.ts +4 -1
- package/dist/rooms/map.d.ts +67 -81
- package/package.json +10 -8
- package/src/Player/ItemManager.ts +55 -16
- package/src/Player/ParameterManager.ts +6 -1
- package/src/Player/Player.ts +164 -148
- package/src/module.ts +13 -0
- package/src/rooms/BaseRoom.ts +120 -0
- package/src/rooms/lobby.ts +11 -1
- package/src/rooms/map.ts +148 -152
- package/tests/change-map.spec.ts +72 -0
- package/tests/item.spec.ts +591 -0
- package/tests/module.spec.ts +38 -0
- package/tests/player-param.spec.ts +28 -0
- package/tests/world-maps.spec.ts +814 -0
- package/vite.config.ts +16 -0
package/src/rooms/map.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { Action, MockConnection, Request, Room, RoomOnJoin } from "@signe/room";
|
|
2
|
-
import { Hooks, IceMovement, ModulesToken, ProjectileMovement, ProjectileType, RpgCommonMap, Direction, RpgCommonPlayer, RpgShape } from "@rpgjs/common";
|
|
1
|
+
import { Action, MockConnection, Request, Room, RoomMethods, RoomOnJoin } from "@signe/room";
|
|
2
|
+
import { Hooks, IceMovement, ModulesToken, ProjectileMovement, ProjectileType, RpgCommonMap, Direction, RpgCommonPlayer, RpgShape, findModules } 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";
|
|
6
6
|
import { signal } from "@signe/reactive";
|
|
7
7
|
import { inject } from "@signe/di";
|
|
8
8
|
import { context } from "../core/context";;
|
|
9
|
-
import { finalize, lastValueFrom } from "rxjs";
|
|
9
|
+
import { finalize, lastValueFrom, throttleTime } 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
13
|
import { z } from "zod";
|
|
14
14
|
import { EntityState } from "@rpgjs/physic";
|
|
15
15
|
import { MapOptions } from "../decorators/map";
|
|
16
|
+
import { BaseRoom } from "./BaseRoom";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Interface for input controls configuration
|
|
@@ -113,7 +114,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
113
114
|
* ```
|
|
114
115
|
*/
|
|
115
116
|
@users(RpgPlayer) players = signal({});
|
|
116
|
-
|
|
117
|
+
|
|
117
118
|
/**
|
|
118
119
|
* Synchronized signal containing all events (NPCs, objects) on the map
|
|
119
120
|
*
|
|
@@ -130,7 +131,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
130
131
|
* ```
|
|
131
132
|
*/
|
|
132
133
|
@sync(RpgPlayer) events = signal({});
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
/**
|
|
135
136
|
* Signal containing the map's database of items, classes, and other game data
|
|
136
137
|
*
|
|
@@ -148,7 +149,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
148
149
|
* ```
|
|
149
150
|
*/
|
|
150
151
|
database = signal({});
|
|
151
|
-
|
|
152
|
+
|
|
152
153
|
/**
|
|
153
154
|
* Array of map configurations - can contain MapOptions objects or instances of map classes
|
|
154
155
|
*
|
|
@@ -156,7 +157,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
156
157
|
* It's populated when the map is loaded via `updateMap()`.
|
|
157
158
|
*/
|
|
158
159
|
maps: (MapOptions | any)[] = []
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
/**
|
|
161
162
|
* Array of sound IDs to play when players join the map
|
|
162
163
|
*
|
|
@@ -170,7 +171,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
170
171
|
* ```
|
|
171
172
|
*/
|
|
172
173
|
sounds: string[] = []
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
/**
|
|
175
176
|
* BehaviorSubject that completes when the map data is ready
|
|
176
177
|
*
|
|
@@ -186,7 +187,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
186
187
|
* ```
|
|
187
188
|
*/
|
|
188
189
|
dataIsReady$ = new BehaviorSubject<void>(undefined);
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
/**
|
|
191
192
|
* Global configuration object for the map
|
|
192
193
|
*
|
|
@@ -194,7 +195,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
194
195
|
* It's populated from the map data when `updateMap()` is called.
|
|
195
196
|
*/
|
|
196
197
|
globalConfig: any = {}
|
|
197
|
-
|
|
198
|
+
|
|
198
199
|
/**
|
|
199
200
|
* Damage formulas configuration for the map
|
|
200
201
|
*
|
|
@@ -207,15 +208,32 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
207
208
|
private _shapes: Map<string, RpgShape> = new Map();
|
|
208
209
|
/** Internal: Map of shape entity UUIDs to RpgShape instances */
|
|
209
210
|
private _shapeEntities: Map<string, RpgShape> = new Map();
|
|
211
|
+
/** Internal: Subscription for the input processing loop */
|
|
212
|
+
private _inputLoopSubscription?: any;
|
|
213
|
+
/** Enable/disable automatic tick processing (useful for unit tests) */
|
|
214
|
+
private _autoTickEnabled: boolean = true;
|
|
215
|
+
|
|
216
|
+
autoSync: boolean = true;
|
|
210
217
|
|
|
211
|
-
constructor() {
|
|
218
|
+
constructor(room) {
|
|
212
219
|
super();
|
|
213
220
|
this.hooks.callHooks("server-map-onStart", this).subscribe();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
const isTest = room.env.TEST === 'true' ? true : false;
|
|
222
|
+
if (isTest) {
|
|
223
|
+
this.autoSync = false;
|
|
224
|
+
this.setAutoTick(false);
|
|
225
|
+
this.throttleSync = 0;
|
|
226
|
+
this.throttleStorage = 0;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.throttleSync = this.isStandalone ? 1 : 50
|
|
230
|
+
this.throttleStorage = this.isStandalone ? 1 : 50
|
|
231
|
+
};
|
|
232
|
+
this.sessionExpiryTime = 1000 * 60 * 5;
|
|
217
233
|
this.setupCollisionDetection();
|
|
218
|
-
this.
|
|
234
|
+
if (this._autoTickEnabled) {
|
|
235
|
+
this.loop();
|
|
236
|
+
}
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
/**
|
|
@@ -285,7 +303,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
285
303
|
// One of the entities is a shape
|
|
286
304
|
const shape = shapeA || shapeB;
|
|
287
305
|
const otherEntity = shapeA ? entityB : entityA;
|
|
288
|
-
|
|
306
|
+
|
|
289
307
|
if (shape) {
|
|
290
308
|
const shapeKey = `${otherEntity.uuid}-${shape.name}`;
|
|
291
309
|
if (!activeShapeCollisions.has(shapeKey)) {
|
|
@@ -321,7 +339,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
321
339
|
if (event) {
|
|
322
340
|
// Mark this collision as processed
|
|
323
341
|
activeCollisions.add(collisionKey);
|
|
324
|
-
|
|
342
|
+
|
|
325
343
|
// Trigger the onPlayerTouch hook on the event
|
|
326
344
|
event.execMethod('onPlayerTouch', [player]);
|
|
327
345
|
}
|
|
@@ -344,7 +362,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
344
362
|
// One of the entities is a shape
|
|
345
363
|
const shape = shapeA || shapeB;
|
|
346
364
|
const otherEntity = shapeA ? entityB : entityA;
|
|
347
|
-
|
|
365
|
+
|
|
348
366
|
if (shape) {
|
|
349
367
|
const shapeKey = `${otherEntity.uuid}-${shape.name}`;
|
|
350
368
|
if (activeShapeCollisions.has(shapeKey)) {
|
|
@@ -461,7 +479,11 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
461
479
|
* ```
|
|
462
480
|
*/
|
|
463
481
|
onJoin(player: RpgPlayer, conn: MockConnection) {
|
|
464
|
-
player.
|
|
482
|
+
if (player.setMap) {
|
|
483
|
+
player.setMap(this);
|
|
484
|
+
} else {
|
|
485
|
+
player.map = this;
|
|
486
|
+
}
|
|
465
487
|
player.context = context;
|
|
466
488
|
player.conn = conn;
|
|
467
489
|
player._onInit()
|
|
@@ -471,17 +493,17 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
471
493
|
if ((this as any).stopAllSoundsBeforeJoin) {
|
|
472
494
|
player.stopAllSounds();
|
|
473
495
|
}
|
|
474
|
-
|
|
475
|
-
this.sounds.forEach(sound => player.playSound(sound,{ loop: true }));
|
|
476
|
-
|
|
496
|
+
|
|
497
|
+
this.sounds.forEach(sound => player.playSound(sound, { loop: true }));
|
|
498
|
+
|
|
477
499
|
// Execute global map hooks (from RpgServer.map)
|
|
478
500
|
await lastValueFrom(this.hooks.callHooks("server-map-onJoin", player, this));
|
|
479
|
-
|
|
501
|
+
|
|
480
502
|
// // Execute map-specific hooks (from @MapData or MapOptions)
|
|
481
503
|
if (typeof (this as any)._onJoin === 'function') {
|
|
482
504
|
await (this as any)._onJoin(player);
|
|
483
505
|
}
|
|
484
|
-
|
|
506
|
+
|
|
485
507
|
// Execute player hooks
|
|
486
508
|
await lastValueFrom(this.hooks.callHooks("server-player-onJoinMap", player, this));
|
|
487
509
|
})
|
|
@@ -514,12 +536,12 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
514
536
|
async onLeave(player: RpgPlayer, conn: MockConnection) {
|
|
515
537
|
// Execute global map hooks (from RpgServer.map)
|
|
516
538
|
await lastValueFrom(this.hooks.callHooks("server-map-onLeave", player, this));
|
|
517
|
-
|
|
539
|
+
|
|
518
540
|
// Execute map-specific hooks (from @MapData or MapOptions)
|
|
519
541
|
if (typeof (this as any)._onLeave === 'function') {
|
|
520
542
|
await (this as any)._onLeave(player);
|
|
521
543
|
}
|
|
522
|
-
|
|
544
|
+
|
|
523
545
|
// Execute player hooks
|
|
524
546
|
await lastValueFrom(this.hooks.callHooks("server-player-onLeaveMap", player, this));
|
|
525
547
|
player.pendingInputs = [];
|
|
@@ -543,89 +565,6 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
543
565
|
return inject<Hooks>(context, ModulesToken);
|
|
544
566
|
}
|
|
545
567
|
|
|
546
|
-
/**
|
|
547
|
-
* Get the width of the map in pixels
|
|
548
|
-
*
|
|
549
|
-
* @returns The width of the map in pixels, or 0 if not loaded
|
|
550
|
-
*
|
|
551
|
-
* @example
|
|
552
|
-
* ```ts
|
|
553
|
-
* const width = map.widthPx;
|
|
554
|
-
* console.log(`Map width: ${width}px`);
|
|
555
|
-
* ```
|
|
556
|
-
*/
|
|
557
|
-
get widthPx(): number {
|
|
558
|
-
return this.data()?.width ?? 0
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Get the height of the map in pixels
|
|
563
|
-
*
|
|
564
|
-
* @returns The height of the map in pixels, or 0 if not loaded
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* ```ts
|
|
568
|
-
* const height = map.heightPx;
|
|
569
|
-
* console.log(`Map height: ${height}px`);
|
|
570
|
-
* ```
|
|
571
|
-
*/
|
|
572
|
-
get heightPx(): number {
|
|
573
|
-
return this.data()?.height ?? 0
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Get the unique identifier of the map
|
|
578
|
-
*
|
|
579
|
-
* @returns The map ID, or empty string if not loaded
|
|
580
|
-
*
|
|
581
|
-
* @example
|
|
582
|
-
* ```ts
|
|
583
|
-
* const mapId = map.id;
|
|
584
|
-
* console.log(`Current map: ${mapId}`);
|
|
585
|
-
* ```
|
|
586
|
-
*/
|
|
587
|
-
get id(): string {
|
|
588
|
-
return this.data()?.id ?? ''
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Get the X position of this map in the world coordinate system
|
|
593
|
-
*
|
|
594
|
-
* This is used when maps are part of a larger world map. The world position
|
|
595
|
-
* indicates where this map is located relative to other maps.
|
|
596
|
-
*
|
|
597
|
-
* @returns The X position in world coordinates, or 0 if not in a world
|
|
598
|
-
*
|
|
599
|
-
* @example
|
|
600
|
-
* ```ts
|
|
601
|
-
* const worldX = map.worldX;
|
|
602
|
-
* console.log(`Map is at world position (${worldX}, ${map.worldY})`);
|
|
603
|
-
* ```
|
|
604
|
-
*/
|
|
605
|
-
get worldX(): number {
|
|
606
|
-
const worldMaps = this.getWorldMapsManager?.();
|
|
607
|
-
return worldMaps?.getMapInfo(this.id)?.worldX ?? 0
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Get the Y position of this map in the world coordinate system
|
|
612
|
-
*
|
|
613
|
-
* This is used when maps are part of a larger world map. The world position
|
|
614
|
-
* indicates where this map is located relative to other maps.
|
|
615
|
-
*
|
|
616
|
-
* @returns The Y position in world coordinates, or 0 if not in a world
|
|
617
|
-
*
|
|
618
|
-
* @example
|
|
619
|
-
* ```ts
|
|
620
|
-
* const worldY = map.worldY;
|
|
621
|
-
* console.log(`Map is at world position (${map.worldX}, ${worldY})`);
|
|
622
|
-
* ```
|
|
623
|
-
*/
|
|
624
|
-
get worldY(): number {
|
|
625
|
-
const worldMaps = this.getWorldMapsManager?.();
|
|
626
|
-
return worldMaps?.getMapInfo(this.id)?.worldY ?? 0
|
|
627
|
-
}
|
|
628
|
-
|
|
629
568
|
/**
|
|
630
569
|
* Handle GUI interaction from a player
|
|
631
570
|
*
|
|
@@ -643,7 +582,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
643
582
|
*/
|
|
644
583
|
@Action('gui.interaction')
|
|
645
584
|
guiInteraction(player: RpgPlayer, value) {
|
|
646
|
-
|
|
585
|
+
this.hooks.callHooks("server-player-guiInteraction", player, value);
|
|
647
586
|
player.syncChanges();
|
|
648
587
|
}
|
|
649
588
|
|
|
@@ -785,6 +724,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
785
724
|
}
|
|
786
725
|
await lastValueFrom(this.hooks.callHooks("server-maps-load", this))
|
|
787
726
|
await lastValueFrom(this.hooks.callHooks("server-worldMaps-load", this))
|
|
727
|
+
await lastValueFrom(this.hooks.callHooks("server-databaseHooks-load", this))
|
|
788
728
|
|
|
789
729
|
map.events = map.events ?? []
|
|
790
730
|
|
|
@@ -805,7 +745,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
805
745
|
else {
|
|
806
746
|
this.sounds = map.sounds ?? []
|
|
807
747
|
}
|
|
808
|
-
|
|
748
|
+
|
|
809
749
|
// Attach map-specific hooks from MapOptions or @MapData
|
|
810
750
|
if (mapFound?.onLoad) {
|
|
811
751
|
(this as any)._onLoad = mapFound.onLoad;
|
|
@@ -830,15 +770,15 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
830
770
|
}
|
|
831
771
|
|
|
832
772
|
this.dataIsReady$.complete()
|
|
833
|
-
|
|
773
|
+
|
|
834
774
|
// Execute global map hooks (from RpgServer.map)
|
|
835
775
|
await lastValueFrom(this.hooks.callHooks("server-map-onLoad", this))
|
|
836
|
-
|
|
776
|
+
|
|
837
777
|
// Execute map-specific hooks (from @MapData or MapOptions)
|
|
838
778
|
if (typeof (this as any)._onLoad === 'function') {
|
|
839
779
|
await (this as any)._onLoad();
|
|
840
780
|
}
|
|
841
|
-
|
|
781
|
+
|
|
842
782
|
// TODO: Update map
|
|
843
783
|
}
|
|
844
784
|
|
|
@@ -1052,25 +992,31 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1052
992
|
/**
|
|
1053
993
|
* Main game loop that processes player inputs
|
|
1054
994
|
*
|
|
1055
|
-
* This private method
|
|
1056
|
-
* for all players on the map. It ensures inputs are
|
|
1057
|
-
* prevents concurrent processing for the same player.
|
|
995
|
+
* This private method subscribes to tick$ and processes pending inputs
|
|
996
|
+
* for all players on the map with a throttle of 50ms. It ensures inputs are
|
|
997
|
+
* processed in order and prevents concurrent processing for the same player.
|
|
1058
998
|
*
|
|
1059
999
|
* ## Architecture
|
|
1060
1000
|
*
|
|
1061
|
-
* -
|
|
1001
|
+
* - Subscribes to tick$ with throttleTime(50ms) for responsive input processing
|
|
1062
1002
|
* - Processes inputs for each player with pending inputs
|
|
1063
1003
|
* - Uses a flag to prevent concurrent processing for the same player
|
|
1064
1004
|
* - Calls `processInput()` to handle anti-cheat validation and movement
|
|
1065
1005
|
*
|
|
1066
1006
|
* @example
|
|
1067
1007
|
* ```ts
|
|
1068
|
-
* // This method is called automatically in the constructor
|
|
1008
|
+
* // This method is called automatically in the constructor if autoTick is enabled
|
|
1069
1009
|
* // You typically don't call it directly
|
|
1070
1010
|
* ```
|
|
1071
1011
|
*/
|
|
1072
1012
|
private loop() {
|
|
1073
|
-
|
|
1013
|
+
if (this._inputLoopSubscription) {
|
|
1014
|
+
this._inputLoopSubscription.unsubscribe();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this._inputLoopSubscription = this.tick$.pipe(
|
|
1018
|
+
throttleTime(50) // Throttle to 50ms for input processing
|
|
1019
|
+
).subscribe(async ({ timestamp }) => {
|
|
1074
1020
|
for (const player of this.getPlayers()) {
|
|
1075
1021
|
if (player.pendingInputs.length > 0) {
|
|
1076
1022
|
const anyPlayer = player as any;
|
|
@@ -1082,7 +1028,35 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1082
1028
|
}
|
|
1083
1029
|
}
|
|
1084
1030
|
}
|
|
1085
|
-
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Enable or disable automatic tick processing
|
|
1036
|
+
*
|
|
1037
|
+
* When disabled, the input processing loop will not run automatically.
|
|
1038
|
+
* This is useful for unit tests where you want manual control over when
|
|
1039
|
+
* inputs are processed.
|
|
1040
|
+
*
|
|
1041
|
+
* @param enabled - Whether to enable automatic tick processing (default: true)
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```ts
|
|
1045
|
+
* // Disable auto tick for testing
|
|
1046
|
+
* map.setAutoTick(false);
|
|
1047
|
+
*
|
|
1048
|
+
* // Manually trigger tick processing
|
|
1049
|
+
* await map.processInput('player1');
|
|
1050
|
+
* ```
|
|
1051
|
+
*/
|
|
1052
|
+
setAutoTick(enabled: boolean): void {
|
|
1053
|
+
this._autoTickEnabled = enabled;
|
|
1054
|
+
if (enabled && !this._inputLoopSubscription) {
|
|
1055
|
+
this.loop();
|
|
1056
|
+
} else if (!enabled && this._inputLoopSubscription) {
|
|
1057
|
+
this._inputLoopSubscription.unsubscribe();
|
|
1058
|
+
this._inputLoopSubscription = undefined;
|
|
1059
|
+
}
|
|
1086
1060
|
}
|
|
1087
1061
|
|
|
1088
1062
|
/**
|
|
@@ -1192,8 +1166,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1192
1166
|
/**
|
|
1193
1167
|
* Add data to the map's database
|
|
1194
1168
|
*
|
|
1195
|
-
* This method
|
|
1196
|
-
* By default, if an ID already exists, the operation is ignored to prevent overwriting existing data.
|
|
1169
|
+
* This method delegates to BaseRoom's implementation to avoid code duplication.
|
|
1197
1170
|
*
|
|
1198
1171
|
* @param id - Unique identifier for the data
|
|
1199
1172
|
* @param data - The data to store (can be a class, object, or any value)
|
|
@@ -1217,23 +1190,13 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1217
1190
|
* ```
|
|
1218
1191
|
*/
|
|
1219
1192
|
addInDatabase(id: string, data: any, options?: { force?: boolean }): boolean {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
// Check if ID already exists
|
|
1223
|
-
if (database[id] !== undefined && !options?.force) {
|
|
1224
|
-
// Ignore the addition if ID exists and force is not enabled
|
|
1225
|
-
return false;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
// Add or overwrite the data
|
|
1229
|
-
database[id] = data;
|
|
1230
|
-
return true;
|
|
1193
|
+
return BaseRoom.prototype.addInDatabase.call(this, id, data, options);
|
|
1231
1194
|
}
|
|
1232
1195
|
|
|
1233
1196
|
/**
|
|
1234
1197
|
* Remove data from the map's database
|
|
1235
1198
|
*
|
|
1236
|
-
* This method
|
|
1199
|
+
* This method delegates to BaseRoom's implementation to avoid code duplication.
|
|
1237
1200
|
*
|
|
1238
1201
|
* @param id - Unique identifier of the data to remove
|
|
1239
1202
|
* @returns true if data was removed, false if ID didn't exist
|
|
@@ -1251,16 +1214,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1251
1214
|
* ```
|
|
1252
1215
|
*/
|
|
1253
1216
|
removeInDatabase(id: string): boolean {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
// Check if ID exists
|
|
1257
|
-
if (database[id] === undefined) {
|
|
1258
|
-
return false;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// Remove the data
|
|
1262
|
-
delete database[id];
|
|
1263
|
-
return true;
|
|
1217
|
+
return BaseRoom.prototype.removeInDatabase.call(this, id);
|
|
1264
1218
|
}
|
|
1265
1219
|
|
|
1266
1220
|
/**
|
|
@@ -1368,7 +1322,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1368
1322
|
|
|
1369
1323
|
eventInstance.x.set(x);
|
|
1370
1324
|
eventInstance.y.set(y);
|
|
1371
|
-
|
|
1325
|
+
|
|
1372
1326
|
this.events()[id] = eventInstance;
|
|
1373
1327
|
|
|
1374
1328
|
await eventInstance.execMethod('onInit')
|
|
@@ -1668,6 +1622,20 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1668
1622
|
}
|
|
1669
1623
|
}
|
|
1670
1624
|
|
|
1625
|
+
/**
|
|
1626
|
+
* Apply sync to the client
|
|
1627
|
+
*
|
|
1628
|
+
* This method applies sync to the client by calling the `$applySync()` method.
|
|
1629
|
+
*
|
|
1630
|
+
* @example
|
|
1631
|
+
* ```ts
|
|
1632
|
+
* map.applySyncToClient();
|
|
1633
|
+
* ```
|
|
1634
|
+
*/
|
|
1635
|
+
applySyncToClient() {
|
|
1636
|
+
this.$applySync();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1671
1639
|
/**
|
|
1672
1640
|
* Create a shape dynamically on the map
|
|
1673
1641
|
*
|
|
@@ -1744,7 +1712,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1744
1712
|
properties?: Record<string, any>;
|
|
1745
1713
|
}): RpgShape {
|
|
1746
1714
|
const { x, y, width, height } = obj;
|
|
1747
|
-
|
|
1715
|
+
|
|
1748
1716
|
// Validate required parameters
|
|
1749
1717
|
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
1750
1718
|
throw new Error('Shape x and y must be numbers');
|
|
@@ -2036,10 +2004,38 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
2036
2004
|
},
|
|
2037
2005
|
});
|
|
2038
2006
|
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Clear all server resources and reset state
|
|
2010
|
+
*
|
|
2011
|
+
* This method should be called to clean up all server-side resources when
|
|
2012
|
+
* shutting down or resetting the map. It stops the input processing loop
|
|
2013
|
+
* and ensures that all subscriptions are properly cleaned up.
|
|
2014
|
+
*
|
|
2015
|
+
* ## Design
|
|
2016
|
+
*
|
|
2017
|
+
* This method is used primarily in testing environments to ensure clean
|
|
2018
|
+
* state between tests. It stops the tick subscription to prevent memory leaks.
|
|
2019
|
+
*
|
|
2020
|
+
* @example
|
|
2021
|
+
* ```ts
|
|
2022
|
+
* // In test cleanup
|
|
2023
|
+
* afterEach(() => {
|
|
2024
|
+
* map.clear();
|
|
2025
|
+
* });
|
|
2026
|
+
* ```
|
|
2027
|
+
*/
|
|
2028
|
+
clear(): void {
|
|
2029
|
+
try {
|
|
2030
|
+
// Stop input processing loop
|
|
2031
|
+
if (this._inputLoopSubscription) {
|
|
2032
|
+
this._inputLoopSubscription.unsubscribe();
|
|
2033
|
+
this._inputLoopSubscription = undefined;
|
|
2034
|
+
}
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
console.warn('Error during map cleanup:', error);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
2039
|
}
|
|
2040
2040
|
|
|
2041
|
-
export interface RpgMap {
|
|
2042
|
-
$send: (conn: MockConnection, data: any) => void;
|
|
2043
|
-
$broadcast: (data: any) => void;
|
|
2044
|
-
$sessionTransfer: (userOrPublicId: any | string, targetRoomId: string) => void;
|
|
2045
|
-
}
|
|
2041
|
+
export interface RpgMap extends RoomMethods { }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { beforeEach, test, expect, afterEach } from 'vitest'
|
|
2
|
+
import { testing } from '@rpgjs/testing'
|
|
3
|
+
import { defineModule, createModule } from '@rpgjs/common'
|
|
4
|
+
import { RpgPlayer, RpgServer } from '../src'
|
|
5
|
+
import { RpgClient } from '../../client/src'
|
|
6
|
+
|
|
7
|
+
// Define server module with two maps
|
|
8
|
+
const serverModule = defineModule<RpgServer>({
|
|
9
|
+
maps: [
|
|
10
|
+
{
|
|
11
|
+
id: 'map1',
|
|
12
|
+
file: '',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'map2',
|
|
16
|
+
file: '',
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
player: {
|
|
20
|
+
async onConnected(player) {
|
|
21
|
+
// Start player on map1
|
|
22
|
+
await player.changeMap('map1', { x: 100, y: 100 })
|
|
23
|
+
},
|
|
24
|
+
onJoinMap(player) {
|
|
25
|
+
console.log('onJoinMap', player.getCurrentMap()?.id)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Define client module
|
|
31
|
+
const clientModule = defineModule<RpgClient>({
|
|
32
|
+
// Client-side logic
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let player: RpgPlayer
|
|
36
|
+
let client: any
|
|
37
|
+
let fixture: any
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
const myModule = createModule('TestModule', [{
|
|
41
|
+
server: serverModule,
|
|
42
|
+
client: clientModule
|
|
43
|
+
}])
|
|
44
|
+
|
|
45
|
+
fixture = await testing(myModule)
|
|
46
|
+
client = await fixture.createClient()
|
|
47
|
+
player = client.player
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
fixture.clear()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('Player can change map', async () => {
|
|
55
|
+
player = await client.waitForMapChange('map1')
|
|
56
|
+
|
|
57
|
+
const initialMap = player.getCurrentMap()
|
|
58
|
+
expect(initialMap).toBeDefined()
|
|
59
|
+
expect(initialMap?.id).toBe('map1')
|
|
60
|
+
|
|
61
|
+
const result = await player.changeMap('map2', { x: 200, y: 200 })
|
|
62
|
+
expect(result).toBe(true)
|
|
63
|
+
|
|
64
|
+
player = await client.waitForMapChange('map2')
|
|
65
|
+
|
|
66
|
+
const newMap = player.getCurrentMap()
|
|
67
|
+
expect(newMap).toBeDefined()
|
|
68
|
+
expect(newMap?.id).toBe('map2')
|
|
69
|
+
|
|
70
|
+
expect(player.x()).toBe(200 - player.hitbox().h / 2)
|
|
71
|
+
expect(player.y()).toBe(200 - player.hitbox().w / 2)
|
|
72
|
+
})
|