@rpgjs/server 5.0.0-alpha.32 → 5.0.0-alpha.35

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.
@@ -1,5 +1,4 @@
1
- export declare class Log {
2
- private id;
3
- private msg;
1
+ export declare class Log extends Error {
2
+ readonly id: string;
4
3
  constructor(id: string, msg: string);
5
4
  }
@@ -1,5 +1,5 @@
1
1
  import { MockConnection, RoomMethods, RoomOnJoin } from '@signe/room';
2
- import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WorldMapConfig } from '../../../common/src';
2
+ import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WeatherState, 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';
@@ -17,6 +17,8 @@ export interface Controls {
17
17
  minTimeBetweenInputs?: number;
18
18
  /** Whether to enable anti-cheat validation */
19
19
  enableAntiCheat?: boolean;
20
+ /** Maximum number of queued inputs processed per server tick */
21
+ maxInputsPerTick?: number;
20
22
  }
21
23
  /**
22
24
  * Interface representing hook methods available for map events
@@ -58,6 +60,9 @@ export type EventPosOption = {
58
60
  */
59
61
  event: EventConstructor | (EventHooks & Record<string, any>);
60
62
  };
63
+ interface WeatherSetOptions {
64
+ sync?: boolean;
65
+ }
61
66
  export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
62
67
  /**
63
68
  * Synchronized signal containing all players currently on the map
@@ -158,6 +163,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
158
163
  * with custom formulas when the map is loaded.
159
164
  */
160
165
  damageFormulas: any;
166
+ private _weatherState;
161
167
  /** Internal: Map of shapes by name */
162
168
  private _shapes;
163
169
  /** Internal: Map of shape entity UUIDs to RpgShape instances */
@@ -169,6 +175,8 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
169
175
  autoSync: boolean;
170
176
  constructor(room: any);
171
177
  onStart(): Promise<void>;
178
+ private isPositiveNumber;
179
+ private resolveTrustedMapDimensions;
172
180
  /**
173
181
  * Setup collision detection between players, events, and shapes
174
182
  *
@@ -391,6 +399,10 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
391
399
  * ```
392
400
  */
393
401
  onInput(player: RpgPlayer, input: any): Promise<void>;
402
+ onPing(player: RpgPlayer, payload: {
403
+ clientTime?: number;
404
+ clientFrame?: number;
405
+ }): void;
394
406
  saveSlot(player: RpgPlayer, value: {
395
407
  requestId: string;
396
408
  index: number;
@@ -463,10 +475,11 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
463
475
  /**
464
476
  * Process pending inputs for a player with anti-cheat validation
465
477
  *
466
- * This method processes all pending inputs for a player while performing
478
+ * This method processes pending inputs for a player while performing
467
479
  * anti-cheat validation to prevent time manipulation and frame skipping.
468
480
  * It validates the time deltas between inputs and ensures they are within
469
- * acceptable ranges.
481
+ * acceptable ranges. To preserve movement itinerary under network bursts,
482
+ * the number of inputs processed per call is capped.
470
483
  *
471
484
  * ## Architecture
472
485
  *
@@ -924,6 +937,27 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
924
937
  x: number;
925
938
  y: number;
926
939
  }, graphic: string, animationName?: string): void;
940
+ private cloneWeatherState;
941
+ /**
942
+ * Get the current map weather state.
943
+ */
944
+ getWeather(): WeatherState | null;
945
+ /**
946
+ * Set the full weather state for this map.
947
+ *
948
+ * When `sync` is true (default), all connected clients receive the new weather.
949
+ */
950
+ setWeather(next: WeatherState | null, options?: WeatherSetOptions): WeatherState | null;
951
+ /**
952
+ * Patch the current weather state.
953
+ *
954
+ * Nested `params` values are merged.
955
+ */
956
+ patchWeather(patch: Partial<WeatherState>, options?: WeatherSetOptions): WeatherState | null;
957
+ /**
958
+ * Clear weather for this map.
959
+ */
960
+ clearWeather(options?: WeatherSetOptions): void;
927
961
  /**
928
962
  * Configure runtime synchronized properties on the map
929
963
  *
@@ -1257,3 +1291,4 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
1257
1291
  }
1258
1292
  export interface RpgMap extends RoomMethods {
1259
1293
  }
1294
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-alpha.32",
3
+ "version": "5.0.0-alpha.35",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,16 +11,16 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.32",
15
- "@rpgjs/physic": "5.0.0-alpha.32",
16
- "@rpgjs/testing": "5.0.0-alpha.32",
14
+ "@rpgjs/common": "5.0.0-alpha.35",
15
+ "@rpgjs/physic": "5.0.0-alpha.35",
16
+ "@rpgjs/testing": "5.0.0-alpha.35",
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
- "zod": "^4.3.5"
23
+ "zod": "^4.3.6"
24
24
  },
25
25
  "devDependencies": {
26
26
  "vite": "^7.3.1",
@@ -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)
@@ -10,6 +10,32 @@ export type ExpCurve = {
10
10
  accelerationB: number;
11
11
  };
12
12
 
13
+ const DEFAULT_EXP_CURVE: ExpCurve = {
14
+ basis: 30,
15
+ extra: 20,
16
+ accelerationA: 30,
17
+ accelerationB: 30
18
+ };
19
+
20
+ function isObject(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === "object" && value !== null;
22
+ }
23
+
24
+ function toValidNumber(value: unknown, fallback: number): number {
25
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
26
+ }
27
+
28
+ function normalizeExpCurve(value: unknown): ExpCurve {
29
+ if (!isObject(value)) return DEFAULT_EXP_CURVE;
30
+
31
+ return {
32
+ basis: toValidNumber(value.basis, DEFAULT_EXP_CURVE.basis),
33
+ extra: toValidNumber(value.extra, DEFAULT_EXP_CURVE.extra),
34
+ accelerationA: toValidNumber(value.accelerationA, DEFAULT_EXP_CURVE.accelerationA),
35
+ accelerationB: toValidNumber(value.accelerationB, DEFAULT_EXP_CURVE.accelerationB)
36
+ };
37
+ }
38
+
13
39
  /**
14
40
  * Interface for Parameter Manager functionality
15
41
  *
@@ -549,10 +575,18 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
549
575
  * ```
550
576
  * @memberof ParameterManager
551
577
  * */
552
- public _expCurveSignal = type(signal<string>('') as any, '_expCurveSignal', { persist: true }, this as any)
578
+ public _expCurveSignal = type(signal<string>(JSON.stringify(DEFAULT_EXP_CURVE)) as any, '_expCurveSignal', { persist: true }, this as any)
553
579
 
554
580
  get expCurve(): ExpCurve {
555
- return JSON.parse(this._expCurveSignal())
581
+ const raw = this._expCurveSignal()
582
+ if (!raw) return DEFAULT_EXP_CURVE
583
+
584
+ try {
585
+ return normalizeExpCurve(JSON.parse(raw))
586
+ }
587
+ catch {
588
+ return DEFAULT_EXP_CURVE
589
+ }
556
590
  }
557
591
 
558
592
  set expCurve(val: ExpCurve) {
@@ -187,7 +187,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
187
187
  constructor() {
188
188
  super();
189
189
 
190
- let lastEmitted: { x: number; y: number } | null = null;
190
+ const initialX = typeof this.x === "function" ? Number(this.x()) || 0 : 0;
191
+ const initialY = typeof this.y === "function" ? Number(this.y()) || 0 : 0;
192
+ let lastEmitted: { x: number; y: number } | null = { x: initialX, y: initialY };
191
193
  let pendingUpdate: { x: number; y: number } | null = null;
192
194
  let updateScheduled = false;
193
195
 
@@ -252,6 +254,8 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
252
254
 
253
255
  setMap(map: RpgMap) {
254
256
  this.map = map;
257
+ // Prevent immediate ping-pong map transfers when spawning near a border.
258
+ this.touchSide = true;
255
259
  }
256
260
 
257
261
  applyFrames() {
@@ -303,12 +307,13 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
303
307
  if (canChange.some(v => v === false)) return false;
304
308
 
305
309
  if (positions && typeof positions === 'object') {
306
- this.teleport(positions)
310
+ await this.teleport(positions)
307
311
  }
308
- await room?.$sessionTransfer(this.conn, realMapId);
312
+ const transferToken = await room?.$sessionTransfer(this.conn, realMapId);
309
313
  this.emit("changeMap", {
310
314
  mapId: realMapId,
311
315
  positions,
316
+ transferToken: typeof transferToken === 'string' ? transferToken : undefined,
312
317
  });
313
318
  return true;
314
319
  }
@@ -321,17 +326,34 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
321
326
  const direction = this.getDirection()
322
327
  const marginLeftRight = map.tileWidth / 2
323
328
  const marginTopDown = map.tileHeight / 2
329
+ const hitbox = this.hitbox()
330
+ const currentX = this.x()
331
+ const currentY = this.y()
332
+ const nearBorder =
333
+ currentX < marginLeftRight ||
334
+ currentX > map.widthPx - hitbox.w - marginLeftRight ||
335
+ currentY < marginTopDown ||
336
+ currentY > map.heightPx - hitbox.h - marginTopDown
337
+
338
+ if (this.touchSide) {
339
+ if (nearBorder) {
340
+ return false
341
+ }
342
+ this.touchSide = false
343
+ }
324
344
 
325
345
  const changeMap = async (adjacent, to) => {
326
- if (this.touchSide) {
346
+ const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
347
+ if (!nextMap) {
327
348
  return false
328
349
  }
329
- this.touchSide = true
330
- const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
331
- if (!nextMap) return false
332
350
  const id = nextMap.id as string
333
351
  const nextMapInfo = worldMaps.getMapInfo(id)
334
- return !!(await this.changeMap(id, to(nextMapInfo)))
352
+ const changed = !!(await this.changeMap(id, to(nextMapInfo)))
353
+ if (changed) {
354
+ this.touchSide = true
355
+ }
356
+ return changed
335
357
  }
336
358
 
337
359
  if (nextPosition.x < marginLeftRight && direction == Direction.Left) {
@@ -1,3 +1,5 @@
1
+ import type { WeatherState } from "@rpgjs/common";
2
+
1
3
  export interface MapOptions {
2
4
  /**
3
5
  * Map identifier. Allows to go to the map (for example with player.changeMap())
@@ -97,6 +99,28 @@ export interface MapOptions {
97
99
  * */
98
100
  sounds?: string[]
99
101
 
102
+ /**
103
+ * Initial weather state for this map.
104
+ *
105
+ * This value is applied when the map is loaded and can later be updated
106
+ * at runtime with `map.setWeather()` from server logic.
107
+ *
108
+ * ```ts
109
+ * @MapData({
110
+ * id: 'forest',
111
+ * file: require('./tmx/forest.tmx'),
112
+ * weather: {
113
+ * effect: 'fog',
114
+ * preset: 'rpgForestFog',
115
+ * params: { density: 1.2, height: 0.75 },
116
+ * transitionMs: 1200
117
+ * }
118
+ * })
119
+ * class ForestMap extends RpgMap {}
120
+ * ```
121
+ */
122
+ weather?: WeatherState | null
123
+
100
124
  /**
101
125
  * Whether to stop all sounds before playing the map sounds when a player joins.
102
126
  *
@@ -278,6 +302,7 @@ export function MapData(options: MapOptions) {
278
302
  target.prototype.file = options.file
279
303
  target.prototype.id = options.id
280
304
  target.prototype.sounds = options.sounds
305
+ target.prototype.weather = options.weather
281
306
  target.prototype.lowMemory = options.lowMemory
282
307
  target.prototype.stopAllSoundsBeforeJoin = options.stopAllSoundsBeforeJoin
283
308
 
@@ -299,4 +324,4 @@ export function MapData(options: MapOptions) {
299
324
  target.prototype.onLeave = options.onLeave
300
325
  }
301
326
  }
302
- }
327
+ }
package/src/logs/log.ts CHANGED
@@ -1,3 +1,10 @@
1
- export class Log {
2
- constructor(private id: string, private msg: string) {}
3
- }
1
+ export class Log extends Error {
2
+ readonly id: string;
3
+
4
+ constructor(id: string, msg: string) {
5
+ super(`[${id}] ${msg}`);
6
+ this.name = "RpgLog";
7
+ this.id = id;
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ }
package/src/module.ts CHANGED
@@ -97,6 +97,7 @@ export function provideServerModules(modules: RpgServerModule[]): FactoryProvide
97
97
  type: MapClass.type,
98
98
  name: MapClass.prototype?.name,
99
99
  sounds: MapClass.prototype?.sounds,
100
+ weather: MapClass.prototype?.weather,
100
101
  lowMemory: MapClass.prototype?.lowMemory,
101
102
  stopAllSoundsBeforeJoin: MapClass.prototype?.stopAllSoundsBeforeJoin,
102
103
  events: MapClass.prototype?._events,