@rpgjs/server 5.0.0-alpha.25 → 5.0.0-alpha.27

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.
@@ -91,10 +91,69 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
91
91
  context?: Context;
92
92
  conn: MockConnection | null = null;
93
93
  touchSide: boolean = false; // Protection against map change loops
94
-
94
+
95
+ /**
96
+ * Computed signal for world X position
97
+ *
98
+ * Calculates the absolute world X position from the map's world position
99
+ * plus the player's local X position. Returns 0 if no map is assigned.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const worldX = player.worldX();
104
+ * console.log(`Player is at world X: ${worldX}`);
105
+ * ```
106
+ */
107
+ get worldPositionX() {
108
+ return this._getComputedWorldPosition('x');
109
+ }
110
+
111
+ /**
112
+ * Computed signal for world Y position
113
+ *
114
+ * Calculates the absolute world Y position from the map's world position
115
+ * plus the player's local Y position. Returns 0 if no map is assigned.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const worldY = player.worldY();
120
+ * console.log(`Player is at world Y: ${worldY}`);
121
+ * ```
122
+ */
123
+ get worldPositionY() {
124
+ return this._getComputedWorldPosition('y');
125
+ }
126
+
127
+ private _worldPositionSignals = new WeakMap<any, any>();
128
+
129
+ private _getComputedWorldPosition(axis: 'x' | 'y') {
130
+ // We use a WeakMap to cache the computed signal per instance
131
+ // This ensures that if the player object is copied (e.g. in tests),
132
+ // the new instance gets its own signal bound to itself.
133
+ if (!this._worldPositionSignals) {
134
+ this._worldPositionSignals = new WeakMap();
135
+ }
136
+
137
+ const key = axis;
138
+ let signals = this._worldPositionSignals.get(this);
139
+ if (!signals) {
140
+ signals = {};
141
+ this._worldPositionSignals.set(this, signals);
142
+ }
143
+
144
+ if (!signals[key]) {
145
+ signals[key] = computed(() => {
146
+ const map = this.map as RpgMap | null;
147
+ const mapWorldPos = map ? (map[axis === 'x' ? 'worldX' : 'worldY'] ?? 0) : 0;
148
+ return mapWorldPos + (this[axis] as any)();
149
+ });
150
+ }
151
+ return signals[key];
152
+ }
153
+
95
154
  /** Internal: Shapes attached to this player */
96
155
  private _attachedShapes: Map<string, RpgShape> = new Map();
97
-
156
+
98
157
  /** Internal: Shapes where this player is currently located */
99
158
  private _inShapes: Set<RpgShape> = new Set();
100
159
  /** Last processed client input timestamp for reconciliation */
@@ -118,10 +177,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
118
177
  super();
119
178
  // Use type assertion to access mixin properties
120
179
  (this as any).expCurve = {
121
- basis: 30,
122
- extra: 20,
123
- accelerationA: 30,
124
- accelerationB: 30
180
+ basis: 30,
181
+ extra: 20,
182
+ accelerationA: 30,
183
+ accelerationB: 30
125
184
  };
126
185
 
127
186
  (this as any).addParameter(MAXHP, MAXHP_CURVE);
@@ -139,7 +198,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
139
198
  combineLatest([this.x.observable, this.y.observable])
140
199
  .subscribe(([x, y]) => {
141
200
  pendingUpdate = { x, y };
142
-
201
+
143
202
  // Schedule a synchronous update using queueMicrotask
144
203
  // This groups multiple rapid changes (x and y in the same tick) into a single frame
145
204
  if (!updateScheduled) {
@@ -163,7 +222,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
163
222
  }
164
223
  })
165
224
  }
166
-
225
+
167
226
  _onInit() {
168
227
  this.hooks.callHooks("server-playerProps-load", this).subscribe();
169
228
  }
@@ -177,6 +236,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
177
236
  return this.map
178
237
  }
179
238
 
239
+ setMap(map: RpgMap) {
240
+ this.map = map;
241
+ }
242
+
180
243
  applyFrames() {
181
244
  this._frames.set(this.frames)
182
245
  this.frames = []
@@ -219,12 +282,12 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
219
282
  ): Promise<any | null | boolean> {
220
283
  const realMapId = 'map-' + mapId;
221
284
  const room = this.getCurrentMap();
222
-
285
+
223
286
  const canChange: boolean[] = await lastValueFrom(this.hooks.callHooks("server-player-canChangeMap", this, {
224
287
  id: mapId,
225
288
  }));
226
289
  if (canChange.some(v => v === false)) return false;
227
-
290
+
228
291
  if (positions && typeof positions === 'object') {
229
292
  this.teleport(positions)
230
293
  }
@@ -236,118 +299,90 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
236
299
  return true;
237
300
  }
238
301
 
239
- /**
240
- * Auto change map when player touches map borders
241
- *
242
- * This method checks if the player touches the current map borders
243
- * and automatically performs a change to the adjacent map if it exists.
244
- *
245
- * @param nextPosition - The next position of the player
246
- * @returns Promise<boolean> - true if a map change occurred
247
- *
248
- * @example
249
- * ```ts
250
- * // Called automatically by the movement system
251
- * const changed = await player.autoChangeMap({ x: newX, y: newY });
252
- * if (changed) {
253
- * console.log('Player changed map automatically');
254
- * }
255
- * ```
256
- */
257
- async autoChangeMap(nextPosition: { x: number; y: number }, forcedDirection?: any): Promise<boolean> {
258
- const map = this.getCurrentMap() as RpgMap; // Cast to access extended properties
259
- if (!map) return false;
260
-
261
- const worldMaps = map.getWorldMapsManager?.();
262
- let ret: boolean = false;
263
-
302
+ async autoChangeMap(nextPosition: Vector2): Promise<boolean> {
303
+ const map = this.getCurrentMap()
304
+ const worldMaps = map?.getInWorldMaps()
305
+ let ret: boolean = false
264
306
  if (worldMaps && map) {
265
- const direction = forcedDirection ?? this.getDirection();
266
- const marginLeftRight = (map.tileWidth ?? 32) / 2;
267
- const marginTopDown = (map.tileHeight ?? 32) / 2;
307
+ const direction = this.getDirection()
308
+ const marginLeftRight = map.tileWidth / 2
309
+ const marginTopDown = map.tileHeight / 2
268
310
 
269
- // Current world position of the player
270
- const worldPositionX = (map.worldX ?? 0) + this.x();
271
- const worldPositionY = (map.worldY ?? 0) + this.y();
272
-
273
- const changeMap = async (directionNumber: number, positionCalculator: (nextMapInfo: any) => {x: number, y: number}) => {
274
- if (this.touchSide) {
275
- return false;
311
+ const changeMap = async (adjacent, to) => {
312
+ if (this.touchSide) {
313
+ return false
314
+ }
315
+ this.touchSide = true
316
+ const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
317
+ if (!nextMap) return false
318
+ const id = nextMap.id as string
319
+ const nextMapInfo = worldMaps.getMapInfo(id)
320
+ return !!(await this.changeMap(id, to(nextMapInfo)))
276
321
  }
277
- this.touchSide = true;
278
322
 
279
- const [nextMap] = worldMaps.getAdjacentMaps(map, directionNumber);
280
- if (!nextMap) {
281
- this.touchSide = false;
282
- return false;
323
+ if (nextPosition.x < marginLeftRight && direction == Direction.Left) {
324
+ ret = await changeMap({
325
+ x: map.worldX - 1,
326
+ y: this.worldPositionY() + 1
327
+ }, nextMapInfo => ({
328
+ x: (nextMapInfo.width) - this.hitbox().w - marginLeftRight,
329
+ y: map.worldY - nextMapInfo.y + nextPosition.y
330
+ }))
283
331
  }
284
-
285
- const id = nextMap.id as string;
286
- const nextMapInfo = worldMaps.getMapInfo(id);
287
- if (!nextMapInfo) {
288
- this.touchSide = false;
289
- return false;
332
+ else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction == Direction.Right) {
333
+ ret = await changeMap({
334
+ x: map.worldX + map.widthPx + 1,
335
+ y: this.worldPositionY() + 1
336
+ }, nextMapInfo => ({
337
+ x: marginLeftRight,
338
+ y: map.worldY - nextMapInfo.y + nextPosition.y
339
+ }))
340
+ }
341
+ else if (nextPosition.y < marginTopDown && direction == Direction.Up) {
342
+ ret = await changeMap({
343
+ x: this.worldPositionX() + 1,
344
+ y: map.worldY - 1
345
+ }, nextMapInfo => ({
346
+ x: map.worldX - nextMapInfo.x + nextPosition.x,
347
+ y: (nextMapInfo.height) - this.hitbox().h - marginTopDown,
348
+ }))
349
+ }
350
+ else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction == Direction.Down) {
351
+ ret = await changeMap({
352
+ x: this.worldPositionX() + 1,
353
+ y: map.worldY + map.heightPx + 1
354
+ }, nextMapInfo => ({
355
+ x: map.worldX - nextMapInfo.x + nextPosition.x,
356
+ y: marginTopDown,
357
+ }))
358
+ }
359
+ else {
360
+ this.touchSide = false
290
361
  }
291
-
292
- const newPosition = positionCalculator(nextMapInfo);
293
- const success = await this.changeMap(id, newPosition);
294
-
295
- // Reset touchSide after a delay to allow the change
296
- setTimeout(() => {
297
- this.touchSide = false;
298
- }, 100);
299
-
300
- return !!success;
301
- };
302
- // Check left border
303
- if (nextPosition.x < marginLeftRight && direction === Direction.Left) {
304
- ret = await changeMap(2, nextMapInfo => ({
305
- x: nextMapInfo.width - (this.hitbox().w) - marginLeftRight,
306
- y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
307
- }));
308
- }
309
- // Check right border
310
- else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction === Direction.Right) {
311
- ret = await changeMap(3, nextMapInfo => ({
312
- x: marginLeftRight,
313
- y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
314
- }));
315
- }
316
- // Check top border
317
- else if (nextPosition.y < marginTopDown && direction === Direction.Up) {
318
- ret = await changeMap(0, nextMapInfo => ({
319
- x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
320
- y: nextMapInfo.height - this.hitbox().h - marginTopDown
321
- }));
322
- }
323
- // Check bottom border
324
- else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction === Direction.Down) {
325
- ret = await changeMap(1, nextMapInfo => ({
326
- x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
327
- y: marginTopDown
328
- }));
329
- }
330
- else {
331
- this.touchSide = false;
332
- }
333
362
  }
334
-
335
- return ret;
336
- }
363
+ return ret
364
+ }
337
365
 
338
366
  async teleport(positions: { x: number; y: number }) {
339
367
  if (!this.map) return false;
340
- if (this.map.physic) {
368
+ if (this.map && this.map.physic) {
341
369
  // Skip collision check for teleportation (allow teleporting through walls)
342
370
  const entity = this.map.physic.getEntityByUUID(this.id);
343
371
  if (entity) {
344
- this.map.physic.teleport(entity, { x: positions.x, y: positions.y });
372
+ const hitbox = typeof this.hitbox === "function" ? this.hitbox() : this.hitbox;
373
+ const width = hitbox?.w ?? 32;
374
+ const height = hitbox?.h ?? 32;
375
+
376
+ // Convert top-left position to center position for physics engine
377
+ // positions.x/y are TOP-LEFT coordinates, but physic.teleport expects CENTER coordinates
378
+ const centerX = positions.x + width / 2;
379
+ const centerY = positions.y + height / 2;
380
+
381
+ this.map.physic.teleport(entity, { x: centerX, y: centerY });
345
382
  }
346
383
  }
347
- else {
348
- this.x.set(positions.x)
349
- this.y.set(positions.y)
350
- }
384
+ this.x.set(positions.x)
385
+ this.y.set(positions.y)
351
386
  // Wait for the frame to be added before applying frames
352
387
  // This ensures the frame is added before applyFrames() is called
353
388
  queueMicrotask(() => {
@@ -440,8 +475,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
440
475
  }
441
476
 
442
477
  databaseById(id: string) {
443
- const map = this.getCurrentMap();
444
- if (!map) return;
478
+ // Use this.map directly to support both RpgMap and LobbyRoom
479
+ const map = this.map as any;
480
+ if (!map || !map.database) return;
445
481
  const data = map.database()[id];
446
482
  if (!data)
447
483
  throw new Error(
@@ -504,7 +540,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
504
540
  // Handle overloaded signature: attachShape(options) or attachShape(id, options)
505
541
  let zoneId: string;
506
542
  let shapeOptions: AttachShapeOptions;
507
-
543
+
508
544
  if (typeof idOrOptions === 'string') {
509
545
  zoneId = idOrOptions;
510
546
  if (!options) {
@@ -542,7 +578,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
542
578
  if (shapeOptions.positioning) {
543
579
  const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
544
580
  const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
545
-
581
+
546
582
  switch (shapeOptions.positioning) {
547
583
  case 'top':
548
584
  offset = new Vector2(0, -playerHeight / 2);
@@ -565,7 +601,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
565
601
 
566
602
  // Get zone manager and create attached zone
567
603
  const zoneManager = map.physic.getZoneManager();
568
-
604
+
569
605
  // Convert direction from Direction enum to string if needed
570
606
  // Direction enum values are already strings ("up", "down", "left", "right")
571
607
  let direction: 'up' | 'down' | 'left' | 'right' = 'down';
@@ -606,7 +642,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
606
642
  entities.forEach((entity) => {
607
643
  const event = map.getEvent<RpgEvent>(entity.uuid);
608
644
  const player = map.getPlayer(entity.uuid);
609
-
645
+
610
646
  if (event) {
611
647
  event.execMethod("onInShape", [shape, this]);
612
648
  // Track that this event is in the shape
@@ -627,7 +663,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
627
663
  entities.forEach((entity) => {
628
664
  const event = map.getEvent<RpgEvent>(entity.uuid);
629
665
  const player = map.getPlayer(entity.uuid);
630
-
666
+
631
667
  if (event) {
632
668
  event.execMethod("onOutShape", [shape, this]);
633
669
  // Remove from tracking
@@ -664,10 +700,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
664
700
  // Store mapping from zoneId to physicZoneId for future reference
665
701
  (this as any)._zoneIdMap = (this as any)._zoneIdMap || new Map();
666
702
  (this as any)._zoneIdMap.set(zoneId, physicZoneId);
667
-
703
+
668
704
  // Store the shape
669
705
  this._attachedShapes.set(zoneId, shape);
670
-
706
+
671
707
  // Update shape position when player moves
672
708
  const updateShapePosition = () => {
673
709
  const currentEntity = map.physic.getEntityByUUID(this.id);
@@ -678,7 +714,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
678
714
  }
679
715
  }
680
716
  };
681
-
717
+
682
718
  // Listen to position changes to update shape position
683
719
  playerEntity.onPositionChange(() => {
684
720
  updateShapePosition();
@@ -686,7 +722,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
686
722
 
687
723
  return shape;
688
724
  }
689
-
725
+
690
726
  /**
691
727
  * Get all shapes attached to this player
692
728
  *
@@ -706,7 +742,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
706
742
  getShapes(): RpgShape[] {
707
743
  return Array.from(this._attachedShapes.values());
708
744
  }
709
-
745
+
710
746
  /**
711
747
  * Get all shapes where this player is currently located
712
748
  *
@@ -1053,13 +1089,13 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
1053
1089
  if (typeof height !== 'number' || height <= 0) {
1054
1090
  throw new Error('setHitbox: height must be a positive number');
1055
1091
  }
1056
-
1092
+
1057
1093
  // Update hitbox signal
1058
1094
  this.hitbox.set({
1059
1095
  w: width,
1060
1096
  h: height,
1061
1097
  });
1062
-
1098
+
1063
1099
  // Update physics entity if map exists
1064
1100
  const map = this.getCurrentMap();
1065
1101
  if (map && map.physic) {
@@ -1081,6 +1117,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
1081
1117
  }, this)
1082
1118
  }
1083
1119
  }
1120
+
1121
+ isEvent(): boolean {
1122
+ return false;
1123
+ }
1084
1124
  }
1085
1125
 
1086
1126
  export class RpgEvent extends RpgPlayer {
@@ -1099,6 +1139,10 @@ export class RpgEvent extends RpgPlayer {
1099
1139
  if (!map) return;
1100
1140
  map.removeEvent(this.id);
1101
1141
  }
1142
+
1143
+ override isEvent(): boolean {
1144
+ return true;
1145
+ }
1102
1146
  }
1103
1147
 
1104
1148
 
@@ -1108,18 +1152,17 @@ export class RpgEvent extends RpgPlayer {
1108
1152
  * Extends the RpgPlayer class with additional interfaces from mixins.
1109
1153
  * This provides proper TypeScript support for all mixin methods and properties.
1110
1154
  */
1111
- export interface RpgPlayer extends
1112
- IVariableManager,
1113
- IMoveManager,
1114
- IGoldManager,
1115
- IComponentManager,
1116
- IGuiManager,
1117
- IItemManager,
1118
- IEffectManager,
1119
- IParameterManager,
1120
- IElementManager,
1121
- ISkillManager,
1122
- IBattleManager,
1123
- IClassManager,
1124
- IStateManager
1125
- {}
1155
+ export interface RpgPlayer extends
1156
+ IVariableManager,
1157
+ IMoveManager,
1158
+ IGoldManager,
1159
+ IComponentManager,
1160
+ IGuiManager,
1161
+ IItemManager,
1162
+ IEffectManager,
1163
+ IParameterManager,
1164
+ IElementManager,
1165
+ ISkillManager,
1166
+ IBattleManager,
1167
+ IClassManager,
1168
+ IStateManager { }
package/src/index.ts CHANGED
@@ -12,4 +12,5 @@ export * from "@signe/reactive";
12
12
  export * from "./Gui";
13
13
  export { RpgShape, RpgModule } from "@rpgjs/common";
14
14
  export * from "./decorators/event";
15
- export * from "./decorators/map";
15
+ export * from "./decorators/map";
16
+ export * from "./Player/MoveManager";
package/src/module.ts CHANGED
@@ -127,6 +127,19 @@ export function provideServerModules(modules: RpgServerModule[]): FactoryProvide
127
127
  }
128
128
  };
129
129
  }
130
+ if (module.database && typeof module.database === 'object') {
131
+ const database = {...module.database};
132
+ module = {
133
+ ...module,
134
+ databaseHooks: {
135
+ load: (engine: RpgMap) => {
136
+ for (const key in database) {
137
+ engine.addInDatabase(key, database[key]);
138
+ }
139
+ },
140
+ }
141
+ };
142
+ }
130
143
  return module;
131
144
  })
132
145
  return modules
@@ -0,0 +1,120 @@
1
+ import { signal } from "@signe/reactive";
2
+
3
+ /**
4
+ * Base class for rooms that need database functionality
5
+ *
6
+ * This class provides common database management functionality that is shared
7
+ * between RpgMap and LobbyRoom. It includes methods for adding and managing
8
+ * items, classes, and other game data in the room's database.
9
+ *
10
+ * ## Architecture
11
+ *
12
+ * Both RpgMap and LobbyRoom need to store game entities (items, classes, skills, etc.)
13
+ * in a database. This base class provides the common implementation to avoid code duplication.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * class MyCustomRoom extends BaseRoom {
18
+ * // Your custom room implementation
19
+ * }
20
+ * ```
21
+ */
22
+ export abstract class BaseRoom {
23
+ /**
24
+ * Signal containing the room's database of items, classes, and other game data
25
+ *
26
+ * This database can be dynamically populated using `addInDatabase()` and
27
+ * `removeInDatabase()` methods. It's used to store game entities like items,
28
+ * classes, skills, etc. that are available in this room.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // Add data to database
33
+ * room.addInDatabase('Potion', PotionClass);
34
+ *
35
+ * // Access database
36
+ * const potion = room.database()['Potion'];
37
+ * ```
38
+ */
39
+ database = signal({});
40
+
41
+ /**
42
+ * Add data to the room's database
43
+ *
44
+ * Adds an item, class, or other game entity to the room's database.
45
+ * If the ID already exists and `force` is not enabled, the addition is ignored.
46
+ *
47
+ * ## Architecture
48
+ *
49
+ * This method is used by the item management system to store item definitions
50
+ * in the room's database. When a player adds an item, the system first checks
51
+ * if the item exists in the database, and if not, adds it using this method.
52
+ *
53
+ * @param id - Unique identifier for the data
54
+ * @param data - The data to add (can be a class, object, etc.)
55
+ * @param options - Optional configuration
56
+ * @param options.force - If true, overwrites existing data with the same ID
57
+ * @returns `true` if data was added, `false` if it was ignored (ID already exists)
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // Add a class to the database
62
+ * room.addInDatabase('Potion', PotionClass);
63
+ *
64
+ * // Add an item object to the database
65
+ * room.addInDatabase('custom-item', {
66
+ * name: 'Custom Item',
67
+ * price: 100
68
+ * });
69
+ *
70
+ * // Force overwrite existing data
71
+ * room.addInDatabase('Potion', UpdatedPotionClass, { force: true });
72
+ * ```
73
+ */
74
+ addInDatabase(id: string, data: any, options?: { force?: boolean }): boolean {
75
+ const database = this.database();
76
+
77
+ // Check if ID already exists
78
+ if (database[id] !== undefined && !options?.force) {
79
+ // Ignore the addition if ID exists and force is not enabled
80
+ return false;
81
+ }
82
+
83
+ // Add or overwrite the data
84
+ database[id] = data;
85
+ this.database.set(database);
86
+ return true;
87
+ }
88
+
89
+ /**
90
+ * Remove data from the room's database
91
+ *
92
+ * This method allows you to remove items or data from the room's database.
93
+ *
94
+ * @param id - Unique identifier of the data to remove
95
+ * @returns `true` if data was removed, `false` if ID didn't exist
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * // Remove an item from the database
100
+ * room.removeInDatabase('Potion');
101
+ *
102
+ * // Check if removal was successful
103
+ * const removed = room.removeInDatabase('custom-item');
104
+ * if (removed) {
105
+ * console.log('Item removed successfully');
106
+ * }
107
+ * ```
108
+ */
109
+ removeInDatabase(id: string): boolean {
110
+ const database = this.database();
111
+
112
+ if (database[id] === undefined) {
113
+ return false;
114
+ }
115
+
116
+ delete database[id];
117
+ this.database.set(database);
118
+ return true;
119
+ }
120
+ }
@@ -5,12 +5,22 @@ import { context } from "../core/context";
5
5
  import { users } from "@signe/sync";
6
6
  import { signal } from "@signe/reactive";
7
7
  import { RpgPlayer } from "../Player/Player";
8
+ import { BaseRoom } from "./BaseRoom";
8
9
 
9
10
  @Room({
10
11
  path: "lobby-{id}",
11
12
  })
12
- export class LobbyRoom {
13
+ export class LobbyRoom extends BaseRoom {
13
14
  @users(RpgPlayer) players = signal({});
15
+ autoSync: boolean = true;
16
+
17
+ constructor(room) {
18
+ super();
19
+ const isTest = room.env.TEST === 'true' ? true : false;
20
+ if (isTest) {
21
+ this.autoSync = false;
22
+ }
23
+ }
14
24
 
15
25
  onJoin(player: RpgPlayer, conn: MockConnection) {
16
26
  player.map = this;