@rpgjs/server 5.0.0-alpha.31 → 5.0.0-alpha.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-alpha.31",
3
+ "version": "5.0.0-alpha.33",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,14 +11,14 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.31",
15
- "@rpgjs/physic": "5.0.0-alpha.31",
16
- "@rpgjs/testing": "5.0.0-alpha.31",
14
+ "@rpgjs/common": "5.0.0-alpha.33",
15
+ "@rpgjs/physic": "5.0.0-alpha.33",
16
+ "@rpgjs/testing": "5.0.0-alpha.33",
17
17
  "@rpgjs/database": "^4.3.0",
18
- "@signe/di": "^2.8.2",
19
- "@signe/reactive": "^2.8.2",
20
- "@signe/room": "^2.8.2",
21
- "@signe/sync": "^2.8.2",
18
+ "@signe/di": "^2.8.3",
19
+ "@signe/reactive": "^2.8.3",
20
+ "@signe/room": "^2.8.3",
21
+ "@signe/sync": "^2.8.3",
22
22
  "rxjs": "^7.8.2",
23
23
  "zod": "^4.3.5"
24
24
  },
@@ -119,7 +119,7 @@ export class MenuGui extends Gui {
119
119
  }))
120
120
  const saveLoad = this.buildSaveLoad(options)
121
121
 
122
- return { menus, items, equips: menuEquips, skills, saveLoad, playerStats: buildStats() }
122
+ return { menus, items, equips: menuEquips, skills, saveLoad, playerStats: buildStats(), expForNextlevel: player.expForNextlevel }
123
123
  }
124
124
 
125
125
  private refreshMenu(clientActionId?: string) {
@@ -3,9 +3,10 @@ import { Gui } from './Gui'
3
3
  import { RpgPlayer } from '../Player/Player'
4
4
 
5
5
  export type ShopSellList = Record<string, number> | Array<{ id: string; multiplier: number }>
6
+ export type ShopItemInput = string | { id?: string; [key: string]: any }
6
7
 
7
8
  export interface ShopGuiOptions {
8
- items: any[]
9
+ items: ShopItemInput[]
9
10
  sell?: ShopSellList
10
11
  sellMultiplier?: number
11
12
  message?: string
@@ -16,7 +17,7 @@ export interface ShopGuiOptions {
16
17
  }
17
18
 
18
19
  export class ShopGui extends Gui {
19
- private itemsInput: any[] = []
20
+ private itemsInput: ShopItemInput[] = []
20
21
  private sellMultipliers: Record<string, number> = {}
21
22
  private baseSellMultiplier = 0.5
22
23
  private messageInput?: string
@@ -65,7 +66,7 @@ export class ShopGui extends Gui {
65
66
  return undefined
66
67
  }
67
68
 
68
- const buildItemData = (item, overrides: { price?: number; quantity?: number } = {}) => {
69
+ const buildItemData = (item: ShopItemInput, overrides: { price?: number; quantity?: number } = {}) => {
69
70
  const rawId = typeof item === 'string'
70
71
  ? item
71
72
  : (typeof item?.id === 'function' ? item.id() : (item?.id ?? item?.name))
@@ -6,8 +6,8 @@ type ActorClass = any;
6
6
  interface PlayerWithMixins extends RpgCommonPlayer {
7
7
  databaseById(id: string): any;
8
8
  addParameter(name: string, { start, end }: { start: number, end: number }): void;
9
- addItem(item: any): void;
10
- equip(item: any, equip: boolean): void;
9
+ addItem(item: any): any;
10
+ equip(itemId: string, equip?: boolean | 'auto'): void;
11
11
  }
12
12
 
13
13
  /**
@@ -108,8 +108,11 @@ export function WithClassManager<TBase extends PlayerCtor>(Base: TBase) {
108
108
  (this as any).addParameter(param, actor.parameters[param]);
109
109
  }
110
110
  for (let item of actor.startingEquipment) {
111
- (this as any).addItem(item);
112
- (this as any).equip(item, true);
111
+ const inventory = (this as any).addItem(item);
112
+ const itemId = inventory?.id?.();
113
+ if (itemId) {
114
+ (this as any).equip(itemId, true);
115
+ }
113
116
  }
114
117
  if (actor.class) this.setClass(actor.class);
115
118
  (this as any)["execMethod"]("onSet", [this], actor);
@@ -581,15 +581,19 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
581
581
  }
582
582
 
583
583
  equip(
584
- itemClass: ItemClass | string,
585
- equip: boolean = true
584
+ itemId: string,
585
+ equip: boolean | 'auto' = true
586
586
  ): void {
587
- const itemId = isString(itemClass) ? itemClass : (itemClass as any).name;
588
- const inventory: Item = this.getItem(itemClass);
587
+ const autoAdd = equip === 'auto';
588
+ const equipState = equip === 'auto' ? true : equip;
589
+ const data = (this as any).databaseById(itemId);
590
+ let inventory: Item = this.getItem(itemId);
591
+ if (!inventory && autoAdd) {
592
+ inventory = this.addItem(itemId, 1);
593
+ }
589
594
  if (!inventory) {
590
595
  throw ItemLog.notInInventory(itemId);
591
596
  }
592
- const data = (this as any).databaseById(itemId);
593
597
  if (data._type == "item") {
594
598
  throw ItemLog.invalidToEquiped(itemId);
595
599
  }
@@ -607,11 +611,11 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
607
611
 
608
612
  const item = inventory;
609
613
 
610
- if ((item as any).equipped && equip) {
614
+ if ((item as any).equipped && equipState) {
611
615
  throw ItemLog.isAlreadyEquiped(itemId);
612
616
  }
613
- (item as any).equipped = equip;
614
- if (!equip) {
617
+ (item as any).equipped = equipState;
618
+ if (!equipState) {
615
619
  const index = this.equipments().findIndex((it) => it.id() == item.id());
616
620
  this.equipments().splice(index, 1);
617
621
  } else {
@@ -619,7 +623,7 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
619
623
  }
620
624
  // Call onEquip hook - use stored instance if available
621
625
  const hookTarget = (item as any)._itemInstance || item;
622
- this["execMethod"]("onEquip", [this, equip], hookTarget);
626
+ this["execMethod"]("onEquip", [this, equipState], hookTarget);
623
627
  }
624
628
  } as unknown as TBase;
625
629
  }
@@ -852,12 +856,13 @@ export interface IItemManager {
852
856
  /**
853
857
  * Equips a weapon or armor on a player
854
858
  *
855
- * Think first to add the item in the inventory with the `addItem()` method before equipping the item.
859
+ * Think first to add the item in the inventory with the `addItem()` method before equipping the item,
860
+ * or pass `"auto"` to add the item if it is missing and equip it.
856
861
  *
857
862
  * The `onEquip()` method is called on the ItemClass when the item is equipped or unequipped.
858
863
  *
859
- * @param itemClass - Item class or string identifier. If string, it's the item ID
860
- * @param equip - Equip the item if `true`, unequip if `false` (default: `true`)
864
+ * @param itemId - Item identifier to resolve from the database
865
+ * @param equip - Equip the item if `true`, unequip if `false`, or `"auto"` to add then equip (default: `true`)
861
866
  * @throws {Object} ItemLog.notInInventory - If the item is not in the inventory
862
867
  * - `id`: `ITEM_NOT_INVENTORY`
863
868
  * - `msg`: Error message
@@ -870,19 +875,17 @@ export interface IItemManager {
870
875
  *
871
876
  * @example
872
877
  * ```ts
873
- * import Sword from 'your-database/sword'
874
- *
875
878
  * try {
876
- * player.addItem(Sword)
877
- * player.equip(Sword)
879
+ * player.addItem('sword')
880
+ * player.equip('sword')
878
881
  * // Later, unequip it
879
- * player.equip(Sword, false)
882
+ * player.equip('sword', false)
880
883
  * } catch (err) {
881
884
  * console.log(err)
882
885
  * }
883
886
  * ```
884
887
  */
885
- equip(itemClass: ItemClass | string, equip?: boolean): void;
888
+ equip(itemId: string, equip?: boolean | 'auto'): void;
886
889
 
887
890
  /**
888
891
  * Get the player's attack (sum of items equipped)
@@ -1183,6 +1183,9 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1183
1183
  private readonly onStuck?: MoveRoutesOptions['onStuck'];
1184
1184
  private readonly stuckTimeout: number;
1185
1185
  private readonly stuckThreshold: number;
1186
+ private remainingDistance = 0;
1187
+ private segmentDirection: Direction | null = null;
1188
+ private segmentStep = 0;
1186
1189
 
1187
1190
  // Frequency wait state
1188
1191
  private waitingForFrequency = false;
@@ -1224,6 +1227,9 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1224
1227
  // Reset frequency wait state when processing a new route
1225
1228
  this.waitingForFrequency = false;
1226
1229
  this.frequencyWaitStartTime = 0;
1230
+ this.remainingDistance = 0;
1231
+ this.segmentDirection = null;
1232
+ this.segmentStep = 0;
1227
1233
 
1228
1234
  // Check if we've completed all routes
1229
1235
  if (this.routeIndex >= this.routes.length) {
@@ -1328,52 +1334,15 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1328
1334
  }
1329
1335
  }
1330
1336
 
1331
- // Calculate target top-left position
1332
- let targetTopLeftX = currentTopLeftX;
1333
- let targetTopLeftY = currentTopLeftY;
1334
-
1335
- switch (moveDirection) {
1336
- case Direction.Right:
1337
- case 'right' as any:
1338
- targetTopLeftX = currentTopLeftX + distance;
1339
- break;
1340
- case Direction.Left:
1341
- case 'left' as any:
1342
- targetTopLeftX = currentTopLeftX - distance;
1343
- break;
1344
- case Direction.Down:
1345
- case 'down' as any:
1346
- targetTopLeftY = currentTopLeftY + distance;
1347
- break;
1348
- case Direction.Up:
1349
- case 'up' as any:
1350
- targetTopLeftY = currentTopLeftY - distance;
1351
- break;
1352
- }
1353
-
1354
- // Convert target top-left to center position for physics engine
1355
- // Get entity to access hitbox dimensions
1356
- const entity = map.physic.getEntityByUUID(this.player.id);
1357
- if (!entity) {
1358
- this.finished = true;
1359
- this.onComplete(false);
1360
- return;
1361
- }
1362
-
1363
- // Get hitbox dimensions for conversion
1364
- const hitbox = this.player.hitbox();
1365
- const hitboxWidth = hitbox?.w ?? 32;
1366
- const hitboxHeight = hitbox?.h ?? 32;
1367
-
1368
- // Convert top-left to center: center = topLeft + (size / 2)
1369
- const targetX = targetTopLeftX + hitboxWidth / 2;
1370
- const targetY = targetTopLeftY + hitboxHeight / 2;
1371
-
1372
- this.currentTarget = { x: targetX, y: targetY }; // Center position for physics engine
1373
- this.currentTargetTopLeft = { x: targetTopLeftX, y: targetTopLeftY }; // Top-left position for player.x() comparison
1374
- this.currentDirection = { x: 0, y: 0 };
1337
+ // Prepare segmented movement (per tile)
1338
+ this.remainingDistance = distance;
1339
+ this.segmentDirection = moveDirection;
1340
+ this.segmentStep = this.getTileStepDistance(playerSpeed);
1341
+ this.setNextSegmentTarget(currentTopLeftX, currentTopLeftY);
1375
1342
 
1376
- this.debugLog(`MOVE direction=${moveDirection} from=(${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)}) to=(${targetTopLeftX.toFixed(1)}, ${targetTopLeftY.toFixed(1)}) dist=${distance.toFixed(1)}`);
1343
+ if (this.currentTargetTopLeft) {
1344
+ this.debugLog(`MOVE direction=${moveDirection} from=(${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)}) to=(${this.currentTargetTopLeft.x.toFixed(1)}, ${this.currentTargetTopLeft.y.toFixed(1)}) dist=${distance.toFixed(1)}`);
1345
+ }
1377
1346
 
1378
1347
  // Reset stuck detection when starting a new movement
1379
1348
  this.lastPosition = null;
@@ -1420,7 +1389,13 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1420
1389
 
1421
1390
  if (frequencyMs > 0 && Date.now() - this.frequencyWaitStartTime >= frequencyMs * this.ratioFrequency) {
1422
1391
  this.waitingForFrequency = false;
1423
- this.processNextRoute();
1392
+ if (this.remainingDistance > 0) {
1393
+ const currentTopLeftX = this.player.x();
1394
+ const currentTopLeftY = this.player.y();
1395
+ this.setNextSegmentTarget(currentTopLeftX, currentTopLeftY);
1396
+ } else {
1397
+ this.processNextRoute();
1398
+ }
1424
1399
  }
1425
1400
  return;
1426
1401
  }
@@ -1478,12 +1453,16 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1478
1453
  this.lastDistanceToTarget = null;
1479
1454
  this.stuckCheckInitialized = false;
1480
1455
 
1481
- // Wait for frequency before processing next route
1456
+ // Wait for frequency before processing next route or segment
1482
1457
  if (!this.finished) {
1483
1458
  const playerFrequency = this.player.frequency;
1484
1459
  if (playerFrequency && playerFrequency > 0) {
1485
1460
  this.waitingForFrequency = true;
1486
1461
  this.frequencyWaitStartTime = Date.now();
1462
+ } else if (this.remainingDistance > 0) {
1463
+ const nextTopLeftX = this.player.x();
1464
+ const nextTopLeftY = this.player.y();
1465
+ this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
1487
1466
  } else {
1488
1467
  // No frequency delay, process immediately
1489
1468
  this.processNextRoute();
@@ -1510,12 +1489,16 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1510
1489
  this.lastDistanceToTarget = null;
1511
1490
  this.stuckCheckInitialized = false;
1512
1491
 
1513
- // Wait for frequency before processing next route
1492
+ // Wait for frequency before processing next route or segment
1514
1493
  if (!this.finished) {
1515
1494
  const playerFrequency = player.frequency;
1516
1495
  if (playerFrequency && playerFrequency > 0) {
1517
1496
  this.waitingForFrequency = true;
1518
1497
  this.frequencyWaitStartTime = Date.now();
1498
+ } else if (this.remainingDistance > 0) {
1499
+ const nextTopLeftX = this.player.x();
1500
+ const nextTopLeftY = this.player.y();
1501
+ this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
1519
1502
  } else {
1520
1503
  // No frequency delay, process immediately
1521
1504
  this.processNextRoute();
@@ -1628,6 +1611,10 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1628
1611
  if (playerFrequency && playerFrequency > 0) {
1629
1612
  this.waitingForFrequency = true;
1630
1613
  this.frequencyWaitStartTime = Date.now();
1614
+ } else if (this.remainingDistance > 0) {
1615
+ const nextTopLeftX = this.player.x();
1616
+ const nextTopLeftY = this.player.y();
1617
+ this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
1631
1618
  } else {
1632
1619
  // No frequency delay, process immediately
1633
1620
  this.processNextRoute();
@@ -1657,6 +1644,69 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
1657
1644
  onFinished(): void {
1658
1645
  this.onComplete(true);
1659
1646
  }
1647
+
1648
+ private getTileStepDistance(playerSpeed: number): number {
1649
+ if (!Number.isFinite(playerSpeed) || playerSpeed <= 0) {
1650
+ return this.tileSize;
1651
+ }
1652
+ const stepsPerTile = Math.max(1, Math.floor(this.tileSize / playerSpeed));
1653
+ return stepsPerTile * playerSpeed;
1654
+ }
1655
+
1656
+ private setNextSegmentTarget(currentTopLeftX: number, currentTopLeftY: number): void {
1657
+ if (!this.segmentDirection || this.remainingDistance <= 0) {
1658
+ return;
1659
+ }
1660
+
1661
+ const map = this.player.getCurrentMap() as any;
1662
+ if (!map) {
1663
+ this.finished = true;
1664
+ this.onComplete(false);
1665
+ return;
1666
+ }
1667
+
1668
+ const entity = map.physic.getEntityByUUID(this.player.id);
1669
+ if (!entity) {
1670
+ this.finished = true;
1671
+ this.onComplete(false);
1672
+ return;
1673
+ }
1674
+
1675
+ const segmentDistance = Math.min(this.segmentStep || this.remainingDistance, this.remainingDistance);
1676
+ let targetTopLeftX = currentTopLeftX;
1677
+ let targetTopLeftY = currentTopLeftY;
1678
+
1679
+ switch (this.segmentDirection) {
1680
+ case Direction.Right:
1681
+ case 'right' as any:
1682
+ targetTopLeftX = currentTopLeftX + segmentDistance;
1683
+ break;
1684
+ case Direction.Left:
1685
+ case 'left' as any:
1686
+ targetTopLeftX = currentTopLeftX - segmentDistance;
1687
+ break;
1688
+ case Direction.Down:
1689
+ case 'down' as any:
1690
+ targetTopLeftY = currentTopLeftY + segmentDistance;
1691
+ break;
1692
+ case Direction.Up:
1693
+ case 'up' as any:
1694
+ targetTopLeftY = currentTopLeftY - segmentDistance;
1695
+ break;
1696
+ }
1697
+
1698
+ const hitbox = this.player.hitbox();
1699
+ const hitboxWidth = hitbox?.w ?? 32;
1700
+ const hitboxHeight = hitbox?.h ?? 32;
1701
+
1702
+ const targetX = targetTopLeftX + hitboxWidth / 2;
1703
+ const targetY = targetTopLeftY + hitboxHeight / 2;
1704
+
1705
+ this.currentTarget = { x: targetX, y: targetY };
1706
+ this.currentTargetTopLeft = { x: targetTopLeftX, y: targetTopLeftY };
1707
+ this.currentDirection = { x: 0, y: 0 };
1708
+ this.remainingDistance -= segmentDistance;
1709
+ }
1660
1710
  }
1661
1711
 
1662
1712
  // Create and add the route movement strategy
@@ -434,13 +434,12 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
434
434
  _param = type(computed(() => {
435
435
  const obj = {}
436
436
  const parameters = this._parametersSignal()
437
+ const allModifiers = this._getAggregatedModifiers()
437
438
  const level = this._level()
438
439
 
439
440
  for (const [name, paramConfig] of Object.entries(parameters)) {
440
441
  let curveVal = Math.floor((paramConfig.end - paramConfig.start) * ((level - 1) / (this.finalLevel - this.initialLevel))) + paramConfig.start
441
442
 
442
- // Apply modifiers from equipment, states, etc.
443
- const allModifiers = this._getAggregatedModifiers()
444
443
  const modifier = allModifiers[name]
445
444
  if (modifier) {
446
445
  if (modifier.rate) curveVal *= modifier.rate
@@ -691,7 +690,6 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
691
690
  get level(): number {
692
691
  return this._level()
693
692
  }
694
-
695
693
  /**
696
694
  * ```ts
697
695
  * console.log(player.expForNextlevel) // 150
package/src/RpgServer.ts CHANGED
@@ -696,10 +696,10 @@ export interface RpgServer {
696
696
  * })
697
697
  * ```
698
698
  *
699
- * @prop { { [dataName]: data } } [database]
699
+ * @prop { { [dataName]: data } | (engine: RpgMap) => { [dataName]: data } | Promise<{ [dataName]: data }> } [database]
700
700
  * @memberof RpgServer
701
701
  * */
702
- database?: object | any[],
702
+ database?: object | any[] | ((engine: RpgMap) => object | any[] | Promise<object | any[]>),
703
703
 
704
704
  /**
705
705
  * Array of all maps. Each element can be either a class (decorated with `@MapData` or not) or a `MapOptions` object
package/src/module.ts CHANGED
@@ -127,14 +127,20 @@ 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};
130
+ if (module.database) {
131
+ const database = module.database;
132
132
  module = {
133
133
  ...module,
134
134
  databaseHooks: {
135
- load: (engine: RpgMap) => {
136
- for (const key in database) {
137
- engine.addInDatabase(key, database[key]);
135
+ load: async (engine: RpgMap) => {
136
+ const data = typeof database === 'function'
137
+ ? await database(engine)
138
+ : database;
139
+ if (!data || typeof data !== 'object') {
140
+ return;
141
+ }
142
+ for (const key in data) {
143
+ engine.addInDatabase(key, data[key]);
138
144
  }
139
145
  },
140
146
  }
@@ -145,4 +151,4 @@ export function provideServerModules(modules: RpgServerModule[]): FactoryProvide
145
151
  return modules
146
152
  });
147
153
  }
148
-
154
+
@@ -385,6 +385,24 @@ describe("Item Management - Equipment", () => {
385
385
  expect((item as any).equipped).toBe(true);
386
386
  });
387
387
 
388
+ test("should auto add and equip item", () => {
389
+ player.equip("TestSword", "auto");
390
+ const item = player.getItem("TestSword");
391
+ expect(item).toBeDefined();
392
+ expect(item?.quantity()).toBe(1);
393
+ expect((item as any).equipped).toBe(true);
394
+ expect(player.equipments().some((eq) => eq.id() === "TestSword")).toBe(
395
+ true
396
+ );
397
+ });
398
+
399
+ test("should not add duplicate when auto equipping existing item", () => {
400
+ player.addItem("TestSword", 2);
401
+ player.equip("TestSword", "auto");
402
+ const item = player.getItem("TestSword");
403
+ expect(item?.quantity()).toBe(2);
404
+ });
405
+
388
406
  test("should throw error when equipping non-existent item", () => {
389
407
  expect(() => {
390
408
  player.equip("TestSword", true);
@@ -588,4 +606,4 @@ describe("Item Management - Edge Cases", () => {
588
606
  expect(player.getItem("TestSword")?.quantity()).toBe(1);
589
607
  expect(player.getItem("TestArmor")?.quantity()).toBe(2);
590
608
  });
591
- });
609
+ });