@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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Base class for rooms that need database functionality
3
+ *
4
+ * This class provides common database management functionality that is shared
5
+ * between RpgMap and LobbyRoom. It includes methods for adding and managing
6
+ * items, classes, and other game data in the room's database.
7
+ *
8
+ * ## Architecture
9
+ *
10
+ * Both RpgMap and LobbyRoom need to store game entities (items, classes, skills, etc.)
11
+ * in a database. This base class provides the common implementation to avoid code duplication.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * class MyCustomRoom extends BaseRoom {
16
+ * // Your custom room implementation
17
+ * }
18
+ * ```
19
+ */
20
+ export declare abstract class BaseRoom {
21
+ /**
22
+ * Signal containing the room's database of items, classes, and other game data
23
+ *
24
+ * This database can be dynamically populated using `addInDatabase()` and
25
+ * `removeInDatabase()` methods. It's used to store game entities like items,
26
+ * classes, skills, etc. that are available in this room.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * // Add data to database
31
+ * room.addInDatabase('Potion', PotionClass);
32
+ *
33
+ * // Access database
34
+ * const potion = room.database()['Potion'];
35
+ * ```
36
+ */
37
+ database: import('@signe/reactive').WritableObjectSignal<{}>;
38
+ /**
39
+ * Add data to the room's database
40
+ *
41
+ * Adds an item, class, or other game entity to the room's database.
42
+ * If the ID already exists and `force` is not enabled, the addition is ignored.
43
+ *
44
+ * ## Architecture
45
+ *
46
+ * This method is used by the item management system to store item definitions
47
+ * in the room's database. When a player adds an item, the system first checks
48
+ * if the item exists in the database, and if not, adds it using this method.
49
+ *
50
+ * @param id - Unique identifier for the data
51
+ * @param data - The data to add (can be a class, object, etc.)
52
+ * @param options - Optional configuration
53
+ * @param options.force - If true, overwrites existing data with the same ID
54
+ * @returns `true` if data was added, `false` if it was ignored (ID already exists)
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * // Add a class to the database
59
+ * room.addInDatabase('Potion', PotionClass);
60
+ *
61
+ * // Add an item object to the database
62
+ * room.addInDatabase('custom-item', {
63
+ * name: 'Custom Item',
64
+ * price: 100
65
+ * });
66
+ *
67
+ * // Force overwrite existing data
68
+ * room.addInDatabase('Potion', UpdatedPotionClass, { force: true });
69
+ * ```
70
+ */
71
+ addInDatabase(id: string, data: any, options?: {
72
+ force?: boolean;
73
+ }): boolean;
74
+ /**
75
+ * Remove data from the room's database
76
+ *
77
+ * This method allows you to remove items or data from the room's database.
78
+ *
79
+ * @param id - Unique identifier of the data to remove
80
+ * @returns `true` if data was removed, `false` if ID didn't exist
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * // Remove an item from the database
85
+ * room.removeInDatabase('Potion');
86
+ *
87
+ * // Check if removal was successful
88
+ * const removed = room.removeInDatabase('custom-item');
89
+ * if (removed) {
90
+ * console.log('Item removed successfully');
91
+ * }
92
+ * ```
93
+ */
94
+ removeInDatabase(id: string): boolean;
95
+ }
@@ -1,6 +1,9 @@
1
1
  import { MockConnection } from '@signe/room';
2
2
  import { RpgPlayer } from '../Player/Player';
3
- export declare class LobbyRoom {
3
+ import { BaseRoom } from './BaseRoom';
4
+ export declare class LobbyRoom extends BaseRoom {
4
5
  players: import('@signe/reactive').WritableObjectSignal<{}>;
6
+ autoSync: boolean;
7
+ constructor(room: any);
5
8
  onJoin(player: RpgPlayer, conn: MockConnection): void;
6
9
  }
@@ -1,5 +1,5 @@
1
- import { MockConnection, RoomOnJoin } from '@signe/room';
2
- import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WorldMapConfig } from '@rpgjs/common';
1
+ import { MockConnection, RoomMethods, RoomOnJoin } from '@signe/room';
2
+ import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WorldMapConfig } from '../../../common/src';
3
3
  import { RpgPlayer, RpgEvent } from '../Player/Player';
4
4
  import { BehaviorSubject } from 'rxjs';
5
5
  import { MapOptions } from '../decorators/map';
@@ -162,7 +162,12 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
162
162
  private _shapes;
163
163
  /** Internal: Map of shape entity UUIDs to RpgShape instances */
164
164
  private _shapeEntities;
165
- constructor();
165
+ /** Internal: Subscription for the input processing loop */
166
+ private _inputLoopSubscription?;
167
+ /** Enable/disable automatic tick processing (useful for unit tests) */
168
+ private _autoTickEnabled;
169
+ autoSync: boolean;
170
+ constructor(room: any);
166
171
  /**
167
172
  * Setup collision detection between players, events, and shapes
168
173
  *
@@ -296,72 +301,6 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
296
301
  * ```
297
302
  */
298
303
  get hooks(): Hooks;
299
- /**
300
- * Get the width of the map in pixels
301
- *
302
- * @returns The width of the map in pixels, or 0 if not loaded
303
- *
304
- * @example
305
- * ```ts
306
- * const width = map.widthPx;
307
- * console.log(`Map width: ${width}px`);
308
- * ```
309
- */
310
- get widthPx(): number;
311
- /**
312
- * Get the height of the map in pixels
313
- *
314
- * @returns The height of the map in pixels, or 0 if not loaded
315
- *
316
- * @example
317
- * ```ts
318
- * const height = map.heightPx;
319
- * console.log(`Map height: ${height}px`);
320
- * ```
321
- */
322
- get heightPx(): number;
323
- /**
324
- * Get the unique identifier of the map
325
- *
326
- * @returns The map ID, or empty string if not loaded
327
- *
328
- * @example
329
- * ```ts
330
- * const mapId = map.id;
331
- * console.log(`Current map: ${mapId}`);
332
- * ```
333
- */
334
- get id(): string;
335
- /**
336
- * Get the X position of this map in the world coordinate system
337
- *
338
- * This is used when maps are part of a larger world map. The world position
339
- * indicates where this map is located relative to other maps.
340
- *
341
- * @returns The X position in world coordinates, or 0 if not in a world
342
- *
343
- * @example
344
- * ```ts
345
- * const worldX = map.worldX;
346
- * console.log(`Map is at world position (${worldX}, ${map.worldY})`);
347
- * ```
348
- */
349
- get worldX(): number;
350
- /**
351
- * Get the Y position of this map in the world coordinate system
352
- *
353
- * This is used when maps are part of a larger world map. The world position
354
- * indicates where this map is located relative to other maps.
355
- *
356
- * @returns The Y position in world coordinates, or 0 if not in a world
357
- *
358
- * @example
359
- * ```ts
360
- * const worldY = map.worldY;
361
- * console.log(`Map is at world position (${map.worldX}, ${worldY})`);
362
- * ```
363
- */
364
- get worldY(): number;
365
304
  /**
366
305
  * Handle GUI interaction from a player
367
306
  *
@@ -543,24 +482,43 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
543
482
  /**
544
483
  * Main game loop that processes player inputs
545
484
  *
546
- * This private method runs continuously every 50ms to process pending inputs
547
- * for all players on the map. It ensures inputs are processed in order and
548
- * prevents concurrent processing for the same player.
485
+ * This private method subscribes to tick$ and processes pending inputs
486
+ * for all players on the map with a throttle of 50ms. It ensures inputs are
487
+ * processed in order and prevents concurrent processing for the same player.
549
488
  *
550
489
  * ## Architecture
551
490
  *
552
- * - Runs every 50ms for responsive input processing
491
+ * - Subscribes to tick$ with throttleTime(50ms) for responsive input processing
553
492
  * - Processes inputs for each player with pending inputs
554
493
  * - Uses a flag to prevent concurrent processing for the same player
555
494
  * - Calls `processInput()` to handle anti-cheat validation and movement
556
495
  *
557
496
  * @example
558
497
  * ```ts
559
- * // This method is called automatically in the constructor
498
+ * // This method is called automatically in the constructor if autoTick is enabled
560
499
  * // You typically don't call it directly
561
500
  * ```
562
501
  */
563
502
  private loop;
503
+ /**
504
+ * Enable or disable automatic tick processing
505
+ *
506
+ * When disabled, the input processing loop will not run automatically.
507
+ * This is useful for unit tests where you want manual control over when
508
+ * inputs are processed.
509
+ *
510
+ * @param enabled - Whether to enable automatic tick processing (default: true)
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * // Disable auto tick for testing
515
+ * map.setAutoTick(false);
516
+ *
517
+ * // Manually trigger tick processing
518
+ * await map.processInput('player1');
519
+ * ```
520
+ */
521
+ setAutoTick(enabled: boolean): void;
564
522
  /**
565
523
  * Get a world manager by id
566
524
  *
@@ -647,8 +605,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
647
605
  /**
648
606
  * Add data to the map's database
649
607
  *
650
- * This method allows you to dynamically add items, classes, or any data to the map's database.
651
- * By default, if an ID already exists, the operation is ignored to prevent overwriting existing data.
608
+ * This method delegates to BaseRoom's implementation to avoid code duplication.
652
609
  *
653
610
  * @param id - Unique identifier for the data
654
611
  * @param data - The data to store (can be a class, object, or any value)
@@ -677,7 +634,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
677
634
  /**
678
635
  * Remove data from the map's database
679
636
  *
680
- * This method allows you to remove items or data from the map's database.
637
+ * This method delegates to BaseRoom's implementation to avoid code duplication.
681
638
  *
682
639
  * @param id - Unique identifier of the data to remove
683
640
  * @returns true if data was removed, false if ID didn't exist
@@ -986,6 +943,17 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
986
943
  * ```
987
944
  */
988
945
  setSync(schema: Record<string, any>): void;
946
+ /**
947
+ * Apply sync to the client
948
+ *
949
+ * This method applies sync to the client by calling the `$applySync()` method.
950
+ *
951
+ * @example
952
+ * ```ts
953
+ * map.applySyncToClient();
954
+ * ```
955
+ */
956
+ applySyncToClient(): void;
989
957
  /**
990
958
  * Create a shape dynamically on the map
991
959
  *
@@ -1244,9 +1212,27 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
1244
1212
  frequency?: number;
1245
1213
  direction?: 'x' | 'y' | 'both';
1246
1214
  }): void;
1215
+ /**
1216
+ * Clear all server resources and reset state
1217
+ *
1218
+ * This method should be called to clean up all server-side resources when
1219
+ * shutting down or resetting the map. It stops the input processing loop
1220
+ * and ensures that all subscriptions are properly cleaned up.
1221
+ *
1222
+ * ## Design
1223
+ *
1224
+ * This method is used primarily in testing environments to ensure clean
1225
+ * state between tests. It stops the tick subscription to prevent memory leaks.
1226
+ *
1227
+ * @example
1228
+ * ```ts
1229
+ * // In test cleanup
1230
+ * afterEach(() => {
1231
+ * map.clear();
1232
+ * });
1233
+ * ```
1234
+ */
1235
+ clear(): void;
1247
1236
  }
1248
- export interface RpgMap {
1249
- $send: (conn: MockConnection, data: any) => void;
1250
- $broadcast: (data: any) => void;
1251
- $sessionTransfer: (userOrPublicId: any | string, targetRoomId: string) => void;
1237
+ export interface RpgMap extends RoomMethods {
1252
1238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-alpha.24",
3
+ "version": "5.0.0-alpha.26",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,13 +11,14 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.24",
15
- "@rpgjs/physic": "5.0.0-alpha.24",
14
+ "@rpgjs/common": "5.0.0-alpha.26",
15
+ "@rpgjs/physic": "5.0.0-alpha.26",
16
+ "@rpgjs/testing": "5.0.0-alpha.26",
16
17
  "@rpgjs/database": "^4.3.0",
17
- "@signe/di": "^2.6.0",
18
- "@signe/reactive": "^2.6.0",
19
- "@signe/room": "^2.6.0",
20
- "@signe/sync": "^2.6.0",
18
+ "@signe/di": "^2.7.2",
19
+ "@signe/reactive": "^2.7.2",
20
+ "@signe/room": "^2.7.2",
21
+ "@signe/sync": "^2.7.2",
21
22
  "rxjs": "^7.8.2",
22
23
  "zod": "^4.1.13"
23
24
  },
@@ -28,6 +29,7 @@
28
29
  "type": "module",
29
30
  "scripts": {
30
31
  "dev": "vite build --watch",
31
- "build": "vite build"
32
+ "build": "vite build",
33
+ "test": "vitest"
32
34
  }
33
35
  }
@@ -206,8 +206,10 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
206
206
  });
207
207
  }
208
208
  addItem(item: ItemClass | ItemObject | string, nb: number = 1): Item {
209
- const map = (this as any).getCurrentMap();
210
- if (!map) {
209
+ // Use this.map directly to support both RpgMap and LobbyRoom
210
+ // If no map, player is in Lobby
211
+ const map = (this as any).getCurrentMap() || (this as any).map;
212
+ if (!map || !map.database) {
211
213
  throw new Error('Player must be on a map to add items');
212
214
  }
213
215
 
@@ -219,11 +221,6 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
219
221
  if (isString(item)) {
220
222
  itemId = item as string;
221
223
  data = (this as any).databaseById(itemId);
222
- if (!data) {
223
- throw new Error(
224
- `The ID=${itemId} data is not found in the database. Add the data in the property "database"`
225
- );
226
- }
227
224
  }
228
225
  // Handle class: create instance and add to database if needed
229
226
  else if (typeof item === 'function' || (item as any).prototype) {
@@ -271,13 +268,34 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
271
268
  let instance: Item;
272
269
 
273
270
  if (existingItem) {
274
- // Item already exists, just update quantity
271
+ // Item already exists, update quantity and merge properties
275
272
  instance = existingItem;
276
273
  instance.quantity.update((it) => it + nb);
274
+
275
+ // Update item properties from merged data (e.g., name, description, price)
276
+ if (data.name !== undefined) {
277
+ instance.name.set(data.name);
278
+ }
279
+ if (data.description !== undefined) {
280
+ instance.description.set(data.description);
281
+ }
282
+ if (data.price !== undefined) {
283
+ instance.price.set(data.price);
284
+ }
285
+
286
+ // Update stored instance if it's an object with hooks
287
+ if (itemInstance && typeof itemInstance === 'object' && !(itemInstance instanceof Function)) {
288
+ (instance as any)._itemInstance = itemInstance;
289
+ // Update hooks if they exist
290
+ if (itemInstance.onAdd) {
291
+ instance.onAdd = itemInstance.onAdd.bind(itemInstance);
292
+ }
293
+ }
277
294
  } else {
278
295
  // Create new item instance
279
296
  instance = new Item(data);
280
297
  instance.id.set(itemId);
298
+ instance.quantity.set(nb);
281
299
 
282
300
  // Attach hooks from class instance or object
283
301
  if (itemInstance) {
@@ -294,6 +312,7 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
294
312
 
295
313
  // Call onAdd hook - use stored instance if available
296
314
  const hookTarget = (instance as any)._itemInstance || instance;
315
+ // Only call onAdd if it exists and is a function
297
316
  (this as any)["execMethod"]("onAdd", [this], hookTarget);
298
317
  return instance;
299
318
  }
@@ -315,7 +334,9 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
315
334
  }
316
335
  // Call onRemove hook - use stored instance if available
317
336
  const hookTarget = (item as any)._itemInstance || item;
318
- this["execMethod"]("onRemove", [this], hookTarget);
337
+ if (hookTarget && typeof hookTarget.onRemove === 'function') {
338
+ this["execMethod"]("onRemove", [this], hookTarget);
339
+ }
319
340
  return this.items()[itemIndex];
320
341
  }
321
342
 
@@ -373,7 +394,13 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
373
394
  getParamItem(name: string): number {
374
395
  let nb = 0;
375
396
  for (let item of this.equipments()) {
376
- nb += item[name] || 0;
397
+ // Retrieve item data from database to get properties like atk, pdef, sdef
398
+ try {
399
+ const itemData = (this as any).databaseById(item.id());
400
+ nb += itemData[name] || 0;
401
+ } catch {
402
+ // If item not in database, skip it
403
+ }
377
404
  }
378
405
  const modifier = (this as any).paramsModifier?.[name];
379
406
  if (modifier) {
@@ -404,19 +431,31 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
404
431
  if (!inventory) {
405
432
  throw ItemLog.notInInventory(itemId);
406
433
  }
407
- const item = inventory;
408
- if ((item as any).consumable === false) {
434
+
435
+ // Retrieve item data from database to check consumable and hitRate
436
+ const itemData = (this as any).databaseById(itemId);
437
+ const consumable = itemData?.consumable;
438
+
439
+ // If consumable is explicitly false, throw error
440
+ if (consumable === false) {
409
441
  throw ItemLog.notUseItem(itemId);
410
442
  }
411
- const hitRate = (item as any).hitRate ?? 1;
412
- const hookTarget = (item as any)._itemInstance || item;
443
+
444
+ // If consumable is undefined and item is not of type 'item', it's not consumable
445
+ if (consumable === undefined && itemData?._type && itemData._type !== 'item') {
446
+ throw ItemLog.notUseItem(itemId);
447
+ }
448
+
449
+ const hitRate = itemData?.hitRate ?? 1;
450
+ const hookTarget = (inventory as any)._itemInstance || inventory;
451
+
413
452
  if (Math.random() > hitRate) {
414
453
  this.removeItem(itemClass);
415
454
  this["execMethod"]("onUseFailed", [this], hookTarget);
416
455
  throw ItemLog.chanceToUseFailed(itemId);
417
456
  }
418
- (this as any).applyEffect?.(item);
419
- (this as any).applyStates?.(this, item);
457
+ (this as any).applyEffect?.(itemData);
458
+ (this as any).applyStates?.(this, itemData);
420
459
  this["execMethod"]("onUse", [this], hookTarget);
421
460
  this.removeItem(itemClass);
422
461
  return inventory;
@@ -580,6 +580,9 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
580
580
  this.hpSignal.set(val)
581
581
  }
582
582
 
583
+ get hp(): number {
584
+ return this.hpSignal()
585
+ }
583
586
 
584
587
  /**
585
588
  * Changes the skill points
@@ -601,7 +604,9 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
601
604
  this.spSignal.set(val)
602
605
  }
603
606
 
604
-
607
+ get sp(): number {
608
+ return this.spSignal()
609
+ }
605
610
 
606
611
  /**
607
612
  * Changing the player's experience.