@rpgjs/server 5.0.0-alpha.10 → 5.0.0-alpha.12

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,67 +1,509 @@
1
1
  import { isString, PlayerCtor } from "@rpgjs/common";
2
+ import { signal, computed, WritableSignal, ComputedSignal } from "@signe/reactive";
2
3
  import { MAXHP, MAXSP } from "../presets";
3
-
4
+ import { sync, type } from "@signe/sync";
4
5
 
5
6
  /**
6
- * Mixin that adds parameter management functionality to a player class.
7
- *
8
- * This mixin provides comprehensive parameter management including:
9
- * - Health Points (HP) and Skill Points (SP) management
10
- * - Experience and level progression system
11
- * - Custom parameter creation and modification
12
- * - Parameter modifiers for temporary stat changes
7
+ * Interface for Parameter Manager functionality
13
8
  *
14
- * @template TBase - The base class constructor type
15
- * @param Base - The base class to extend
16
- * @returns A new class that extends the base with parameter management capabilities
17
- *
18
- * @example
19
- * ```ts
20
- * class MyPlayer extends WithParameterManager(BasePlayer) {
21
- * constructor() {
22
- * super();
23
- * this.addParameter('strength', { start: 10, end: 100 });
24
- * }
25
- * }
26
- * ```
9
+ * Provides comprehensive parameter management including health points (HP), skill points (SP),
10
+ * experience and level progression, custom parameters, and parameter modifiers.
27
11
  */
12
+ export interface IParameterManager {
13
+ /**
14
+ * ```ts
15
+ * player.initialLevel = 5
16
+ * ```
17
+ *
18
+ * @title Set initial level
19
+ * @prop {number} player.initialLevel
20
+ * @default 1
21
+ * @memberof ParameterManager
22
+ * */
23
+ initialLevel: number;
24
+
25
+ /**
26
+ * ```ts
27
+ * player.finalLevel = 50
28
+ * ```
29
+ *
30
+ * @title Set final level
31
+ * @prop {number} player.finalLevel
32
+ * @default 99
33
+ * @memberof ParameterManager
34
+ * */
35
+ finalLevel: number;
36
+
37
+ /**
38
+ * With Object-based syntax, you can use following options:
39
+ * - `basis: number`
40
+ * - `extra: number`
41
+ * - `accelerationA: number`
42
+ * - `accelerationB: number`
43
+ * @title Change Experience Curve
44
+ * @prop {object} player.expCurve
45
+ * @default
46
+ * ```ts
47
+ * {
48
+ * basis: 30,
49
+ * extra: 20,
50
+ * accelerationA: 30,
51
+ * accelerationB: 30
52
+ * }
53
+ * ```
54
+ * @memberof ParameterManager
55
+ * */
56
+ expCurve: {
57
+ basis: number,
58
+ extra: number,
59
+ accelerationA: number
60
+ accelerationB: number
61
+ };
62
+
63
+ /**
64
+ * Changes the health points
65
+ * - Cannot exceed the MaxHP parameter
66
+ * - Cannot have a negative value
67
+ * - If the value is 0, a hook named `onDead()` is called in the RpgPlayer class.
68
+ *
69
+ * ```ts
70
+ * player.hp = 100
71
+ * ```
72
+ * @title Change HP
73
+ * @prop {number} player.hp
74
+ * @default MaxHPValue
75
+ * @memberof ParameterManager
76
+ * */
77
+ hp: number;
78
+
79
+ /**
80
+ * Changes the skill points
81
+ * - Cannot exceed the MaxSP parameter
82
+ * - Cannot have a negative value
83
+ *
84
+ * ```ts
85
+ * player.sp = 200
86
+ * ```
87
+ * @title Change SP
88
+ * @prop {number} player.sp
89
+ * @default MaxSPValue
90
+ * @memberof ParameterManager
91
+ * */
92
+ sp: number;
93
+
94
+ /**
95
+ * Changing the player's experience.
96
+ * ```ts
97
+ * player.exp += 100
98
+ * ```
99
+ *
100
+ * Levels are based on the experience curve.
101
+ *
102
+ * ```ts
103
+ * console.log(player.level) // 1
104
+ * console.log(player.expForNextlevel) // 150
105
+ * player.exp += 160
106
+ * console.log(player.level) // 2
107
+ * ```
108
+ *
109
+ * @title Change Experience
110
+ * @prop {number} player.exp
111
+ * @default 0
112
+ * @memberof ParameterManager
113
+ * */
114
+ exp: number;
115
+
116
+ /**
117
+ * Changing the player's level.
118
+ *
119
+ * ```ts
120
+ * player.level += 1
121
+ * ```
122
+ *
123
+ * The level will be between the initial level given by the `initialLevel` and final level given by `finalLevel`
124
+ *
125
+ * ```ts
126
+ * player.finalLevel = 50
127
+ * player.level = 60
128
+ * console.log(player.level) // 50
129
+ * ```
130
+ *
131
+ * @title Change Level
132
+ * @prop {number} player.level
133
+ * @default 1
134
+ * @memberof ParameterManager
135
+ * */
136
+ level: number;
137
+
138
+ /**
139
+ * ```ts
140
+ * console.log(player.expForNextlevel) // 150
141
+ * ```
142
+ * @title Experience for next level ?
143
+ * @prop {number} player.expForNextlevel
144
+ * @readonly
145
+ * @memberof ParameterManager
146
+ * */
147
+ readonly expForNextlevel: number;
148
+
149
+ /**
150
+ * Read the value of a parameter. Put the name of the parameter.
151
+ *
152
+ * ```ts
153
+ * import { Presets } from '@rpgjs/server'
154
+ *
155
+ * const { MAXHP } = Presets
156
+ *
157
+ * console.log(player.param[MAXHP])
158
+ * ```
159
+ *
160
+ * > Possible to use the `player.getParamValue(name)` method instead
161
+ * @title Get Param Value
162
+ * @prop {object} player.param
163
+ * @readonly
164
+ * @memberof ParameterManager
165
+ * */
166
+ readonly param: { [key: string]: number };
167
+
168
+ /**
169
+ * Direct parameter modifiers (reactive signal)
170
+ *
171
+ * > It is important that these parameters have been created beforehand with the `addParameter()` method.
172
+ * > By default, the following settings have been created:
173
+ * - maxhp
174
+ * - maxsp
175
+ * - str
176
+ * - int
177
+ * - dex
178
+ * - agi
179
+ *
180
+ * **Object Key**
181
+ *
182
+ * The key of the object is the name of the parameter
183
+ *
184
+ * > The good practice is to retrieve the name coming from a constant
185
+ *
186
+ * **Object Value**
187
+ *
188
+ * The value of the key is an object containing:
189
+ * ```
190
+ * {
191
+ * value: number,
192
+ * rate: number
193
+ * }
194
+ * ```
195
+ *
196
+ * - value: Adds a number to the parameter
197
+ * - rate: Adds a rate to the parameter
198
+ *
199
+ * > Note that you can put both (value and rate)
200
+ *
201
+ * This property uses reactive signals - changes automatically trigger parameter recalculation.
202
+ * The final parameter values in `param` include aggregated modifiers from equipment, states, etc.
203
+ *
204
+ * @prop {Object} [paramsModifier]
205
+ * @example
206
+ *
207
+ * ```ts
208
+ * import { Presets } from '@rpgjs/server'
209
+ *
210
+ * const { MAXHP } = Presets
211
+ *
212
+ * // Set direct modifiers (reactive)
213
+ * player.paramsModifier = {
214
+ * [MAXHP]: {
215
+ * value: 100
216
+ * }
217
+ * }
218
+ *
219
+ * // Parameters automatically recalculate
220
+ * console.log(player.param[MAXHP]); // Updated value
221
+ * ```
222
+ *
223
+ * @title Set Parameters Modifier
224
+ * @prop {object} paramsModifier
225
+ * @memberof ParameterManager
226
+ * */
227
+ paramsModifier: {
228
+ [key: string]: {
229
+ value?: number,
230
+ rate?: number
231
+ }
232
+ };
233
+
234
+ /**
235
+ * Get or set the parameters object
236
+ *
237
+ * @prop {object} parameters
238
+ * @memberof ParameterManager
239
+ */
240
+ parameters: { [key: string]: { start: number, end: number } };
241
+
242
+ /**
243
+ * Get the value of a specific parameter by name
244
+ *
245
+ * @deprecated Use `player.param[name]` instead for better reactivity
246
+ * @param name - The name of the parameter to get
247
+ * @returns The calculated parameter value
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * import { Presets } from '@rpgjs/server'
252
+ *
253
+ * const { MAXHP } = Presets
254
+ *
255
+ * // Preferred way (reactive)
256
+ * const maxHp = player.param[MAXHP];
257
+ *
258
+ * // Legacy way (still works)
259
+ * const maxHp = player.getParamValue(MAXHP);
260
+ * ```
261
+ */
262
+ getParamValue(name: string): number;
263
+
264
+ /**
265
+ * Give a new parameter. Give a start value and an end value.
266
+ * The start value will be set to the level set at `player.initialLevel` and the end value will be linked to the level set at `player.finalLevel`.
267
+ *
268
+ * ```ts
269
+ * const SPEED = 'speed'
270
+ *
271
+ * player.addParameter(SPEED, {
272
+ * start: 10,
273
+ * end: 100
274
+ * })
275
+ *
276
+ * player.param[SPEED] // 10
277
+ * player.level += 5
278
+ * player.param[SPEED] // 14
279
+ * ```
280
+ *
281
+ * @title Add custom parameters
282
+ * @method player.addParameter(name,curve)
283
+ * @param {string} name - The name of the parameter
284
+ * @param {object} curve - Scheme of the object: { start: number, end: number }
285
+ * @returns {void}
286
+ * @memberof ParameterManager
287
+ * */
288
+ addParameter(name: string, curve: { start: number, end: number }): void;
289
+
290
+ /**
291
+ * Gives back in percentage of health points to skill points
292
+ *
293
+ * ```ts
294
+ * import { Presets } from '@rpgjs/server'
295
+ *
296
+ * const { MAXHP } = Presets
297
+ *
298
+ * console.log(player.param[MAXHP]) // 800
299
+ * player.hp = 100
300
+ * player.recovery({ hp: 0.5 }) // = 800 * 0.5
301
+ * console.log(player.hp) // 400
302
+ * ```
303
+ *
304
+ * @title Recovery HP and/or SP
305
+ * @method player.recovery(params)
306
+ * @param {object} params - Scheme of the object: { hp: number, sp: number }. The values of the numbers must be in 0 and 1
307
+ * @returns {void}
308
+ * @memberof ParameterManager
309
+ * */
310
+ recovery(params: { hp?: number, sp?: number }): void;
311
+
312
+ /**
313
+ * restores all HP and SP
314
+ *
315
+ * ```ts
316
+ * import { Presets } from '@rpgjs/server'
317
+ *
318
+ * const { MAXHP, MAXSP } = Presets
319
+ *
320
+ * console.log(player.param[MAXHP], player.param[MAXSP]) // 800, 230
321
+ * player.hp = 100
322
+ * player.sp = 0
323
+ * player.allRecovery()
324
+ * console.log(player.hp, player.sp) // 800, 230
325
+ * ```
326
+ *
327
+ * @title All Recovery
328
+ * @method player.allRecovery()
329
+ * @returns {void}
330
+ * @memberof ParameterManager
331
+ * */
332
+ allRecovery(): void;
333
+ }
334
+
28
335
  /**
29
- * Parameter Manager Mixin
336
+ * Parameter Manager Mixin with Reactive Signals
337
+ *
338
+ * Provides comprehensive parameter management functionality using reactive signals from `@signe/reactive`.
339
+ * This mixin handles health points (HP), skill points (SP), experience and level progression,
340
+ * custom parameters, and parameter modifiers with automatic reactivity.
30
341
  *
31
- * Provides comprehensive parameter management functionality to any class. This mixin handles
32
- * health points (HP), skill points (SP), experience and level progression, custom parameters,
33
- * and parameter modifiers for temporary stat changes.
342
+ * **Key Features:**
343
+ * - **Reactive Parameters**: All parameters automatically recalculate when level or modifiers change
344
+ * - 🚀 **Performance Optimized**: Uses computed signals to avoid unnecessary recalculations
345
+ * - 🔄 **Real-time Updates**: Changes propagate automatically throughout the system
346
+ * - 🎯 **Type Safe**: Full TypeScript support with proper type inference
34
347
  *
348
+ * @template TBase - The base class constructor type
35
349
  * @param Base - The base class to extend with parameter management
36
- * @returns Extended class with parameter management methods
350
+ * @returns Extended class with reactive parameter management methods
37
351
  *
38
352
  * @example
39
353
  * ```ts
40
354
  * class MyPlayer extends WithParameterManager(BasePlayer) {
41
355
  * constructor() {
42
356
  * super();
357
+ *
358
+ * // Add custom parameters
43
359
  * this.addParameter('strength', { start: 10, end: 100 });
360
+ * this.addParameter('magic', { start: 5, end: 80 });
44
361
  * }
45
362
  * }
46
363
  *
47
364
  * const player = new MyPlayer();
48
- * player.hp = 100;
365
+ *
366
+ * // Reactive parameter updates
49
367
  * player.level = 5;
368
+ * console.log(player.param.strength); // Automatically calculated for level 5
369
+ *
370
+ * // Reactive modifiers
371
+ * player.paramsModifier = {
372
+ * [MAXHP]: { value: 100, rate: 1.2 }
373
+ * };
374
+ * console.log(player.param[MAXHP]); // Automatically includes modifiers
50
375
  * ```
51
376
  */
52
377
  export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
53
378
  return class extends Base {
54
- _paramsModifier: {
379
+ /**
380
+ * Signal for parameter modifiers - allows reactive updates when modifiers change
381
+ *
382
+ * This signal tracks temporary parameter modifications from equipment, states, etc.
383
+ * When updated, it automatically triggers recalculation of all computed parameters.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * // Set modifier that adds 100 to MaxHP
388
+ * player.paramsModifier = {
389
+ * [MAXHP]: { value: 100 }
390
+ * };
391
+ *
392
+ * // Parameters automatically recalculate
393
+ * console.log(player.param[MAXHP]); // Updated value
394
+ * ```
395
+ */
396
+ private _paramsModifierSignal = signal<{
55
397
  [key: string]: {
56
398
  value?: number,
57
399
  rate?: number
58
400
  }
59
- } = {}
401
+ }>({})
60
402
 
61
- _parameters: Map<string, {
62
- start: number,
63
- end: number
64
- }> = new Map()
403
+ /**
404
+ * Signal for base parameters configuration
405
+ *
406
+ * Stores the start and end values for each parameter's level curve.
407
+ * Changes to this signal trigger recalculation of all parameter values.
408
+ */
409
+ private _parametersSignal = signal<{
410
+ [key: string]: {
411
+ start: number,
412
+ end: number
413
+ }
414
+ }>({})
415
+
416
+ /**
417
+ * Computed signal for all parameter values
418
+ *
419
+ * Automatically recalculates all parameter values when level or modifiers change.
420
+ * This provides reactive parameter updates throughout the system.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * // Access reactive parameters
425
+ * const maxHp = player.param[MAXHP]; // Always current value
426
+ *
427
+ * // Parameters update automatically when level changes
428
+ * player.level = 10;
429
+ * console.log(player.param[MAXHP]); // New calculated value
430
+ * ```
431
+ */
432
+ _param = type(computed(() => {
433
+ const obj = {}
434
+ const parameters = this._parametersSignal()
435
+ const level = this._level()
436
+
437
+ for (const [name, paramConfig] of Object.entries(parameters)) {
438
+ let curveVal = Math.floor((paramConfig.end - paramConfig.start) * ((level - 1) / (this.finalLevel - this.initialLevel))) + paramConfig.start
439
+
440
+ // Apply modifiers from equipment, states, etc.
441
+ const allModifiers = this._getAggregatedModifiers()
442
+ const modifier = allModifiers[name]
443
+ if (modifier) {
444
+ if (modifier.rate) curveVal *= modifier.rate
445
+ if (modifier.value) curveVal += modifier.value
446
+ }
447
+
448
+ obj[name] = curveVal
449
+ }
450
+
451
+ return obj
452
+ }) as any, '_param', {}, this as any)
453
+
454
+ /**
455
+ * Aggregates parameter modifiers from all sources (direct modifiers, states, equipment)
456
+ *
457
+ * This method combines modifiers from multiple sources and calculates the final
458
+ * modifier values for each parameter. It handles both value and rate modifiers.
459
+ *
460
+ * @returns Aggregated parameter modifiers
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * // Internal usage - gets modifiers from all sources
465
+ * const modifiers = this._getAggregatedModifiers();
466
+ * console.log(modifiers[MAXHP]); // { value: 100, rate: 1.2 }
467
+ * ```
468
+ */
469
+ private _getAggregatedModifiers(): { [key: string]: { value?: number, rate?: number } } {
470
+ const params = {}
471
+ const paramsAvg = {}
472
+
473
+ const changeParam = (paramsModifier) => {
474
+ for (let key in paramsModifier) {
475
+ const { rate, value } = paramsModifier[key]
476
+ if (!params[key]) params[key] = { rate: 0, value: 0 }
477
+ if (!paramsAvg[key]) paramsAvg[key] = 0
478
+ if (value) params[key].value += value
479
+ if (rate !== undefined) params[key].rate += rate
480
+ paramsAvg[key]++
481
+ }
482
+ }
483
+
484
+ const getModifier = (prop) => {
485
+ if (!isString(prop)) {
486
+ changeParam(prop)
487
+ return
488
+ }
489
+ for (let el of this[prop]()) {
490
+ if (!el.paramsModifier) continue
491
+ changeParam(el.paramsModifier)
492
+ }
493
+ }
494
+
495
+ // Aggregate modifiers from all sources
496
+ getModifier(this._paramsModifierSignal())
497
+ getModifier('states')
498
+ getModifier('equipments')
499
+
500
+ // Average the rates
501
+ for (let key in params) {
502
+ params[key].rate /= paramsAvg[key]
503
+ }
504
+
505
+ return params
506
+ }
65
507
 
66
508
  /**
67
509
  * ```ts
@@ -135,12 +577,9 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
135
577
  this['execMethod']('onDead')
136
578
  val = 0
137
579
  }
138
- (this as any)._hp.set(val)
580
+ this.hpSignal.set(val)
139
581
  }
140
582
 
141
- get hp(): number {
142
- return (this as any)._hp()
143
- }
144
583
 
145
584
  /**
146
585
  * Changes the skill points
@@ -159,12 +598,10 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
159
598
  if (val > this.param[MAXSP]) {
160
599
  val = this.param[MAXSP]
161
600
  }
162
- this._sp.set(val)
601
+ this.spSignal.set(val)
163
602
  }
164
603
 
165
- get sp(): number {
166
- return this._sp()
167
- }
604
+
168
605
 
169
606
  /**
170
607
  * Changing the player's experience.
@@ -276,43 +713,11 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
276
713
  * @memberof ParameterManager
277
714
  * */
278
715
  get param() {
279
- const obj = {}
280
- this._parameters.forEach((val, name) => {
281
- obj[name] = this.getParamValue(name)
282
- })
283
- return obj
716
+ return this._param()
284
717
  }
285
718
 
286
719
  get paramsModifier() {
287
- const params = {}
288
- const paramsAvg = {}
289
- const changeParam = (paramsModifier) => {
290
- for (let key in paramsModifier) {
291
- const { rate, value } = paramsModifier[key]
292
- if (!params[key]) params[key] = { rate: 0, value: 0 }
293
- if (!paramsAvg[key]) paramsAvg[key] = 0
294
- if (value) params[key].value += value
295
- if (rate !== undefined) params[key].rate += rate
296
- paramsAvg[key]++
297
- }
298
- }
299
- const getModifier = (prop) => {
300
- if (!isString(prop)) {
301
- changeParam(prop)
302
- return
303
- }
304
- for (let el of this[prop]()) {
305
- if (!el.paramsModifier) continue
306
- changeParam(el.paramsModifier)
307
- }
308
- }
309
- getModifier(this._paramsModifier)
310
- getModifier('states')
311
- getModifier('equipments')
312
- for (let key in params) {
313
- params[key].rate /= paramsAvg[key]
314
- }
315
- return params
720
+ return this._paramsModifierSignal()
316
721
  }
317
722
 
318
723
  /**
@@ -378,15 +783,15 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
378
783
  rate?: number
379
784
  }
380
785
  }) {
381
- this._paramsModifier = val
786
+ this._paramsModifierSignal.set(val)
382
787
  }
383
788
 
384
789
  get parameters() {
385
- return this._parameters
790
+ return this._parametersSignal()
386
791
  }
387
792
 
388
793
  set parameters(val) {
389
- this._parameters = val
794
+ this._parametersSignal.set(val)
390
795
  }
391
796
 
392
797
  private _expForLevel(level: number): number {
@@ -399,23 +804,8 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
399
804
  return Math.round(basis * (Math.pow(level - 1, 0.9 + accelerationA / 250)) * level * (level + 1) / (6 + Math.pow(level, 2) / 50 / accelerationB) + (level - 1) * extra)
400
805
  }
401
806
 
402
- private getParam(name: string) {
403
- const features = this._parameters.get(name)
404
- if (!features) {
405
- throw `Parameter ${name} not exists. Please use addParameter() before`
406
- }
407
- return features
408
- }
409
-
410
807
  getParamValue(name: string): number | never {
411
- const features = this.getParam(name)
412
- let curveVal = Math.floor((features.end - features.start) * ((this.level-1) / (this.finalLevel - this.initialLevel))) + features.start
413
- const modifier = this.paramsModifier[name]
414
- if (modifier) {
415
- if (modifier.rate) curveVal *= modifier.rate
416
- if (modifier.value) curveVal += modifier.value
417
- }
418
- return curveVal
808
+ return this.param[name]
419
809
  }
420
810
 
421
811
  /**
@@ -437,15 +827,17 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
437
827
  *
438
828
  * @title Add custom parameters
439
829
  * @method player.addParameter(name,curve)
440
- * @param {name} name
441
- * @param {object} curve Scheme of the object: { start: number, end: number }
830
+ * @param {string} name - The name of the parameter
831
+ * @param {object} curve - Scheme of the object: { start: number, end: number }
442
832
  * @returns {void}
443
833
  * @memberof ParameterManager
444
834
  * */
445
835
  addParameter(name: string, { start, end }: { start: number, end: number }): void {
446
- this._parameters.set(name, {
447
- start,
448
- end
836
+ this._parametersSignal.mutate(parameters => {
837
+ parameters[name] = {
838
+ start,
839
+ end
840
+ }
449
841
  })
450
842
  const maxHp = this.param[MAXHP]
451
843
  const maxSp = this.param[MAXSP]
@@ -473,7 +865,7 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
473
865
  *
474
866
  * @title Recovery HP and/or SP
475
867
  * @method player.recovery(params)
476
- * @param {object} params Scheme of the object: { hp: number, sp: number }. The values of the numbers must be in 0 and 1
868
+ * @param {object} params - Scheme of the object: { hp: number, sp: number }. The values of the numbers must be in 0 and 1
477
869
  * @returns {void}
478
870
  * @memberof ParameterManager
479
871
  * */
@@ -506,6 +898,4 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
506
898
  this.recovery({ hp: 1, sp: 1 })
507
899
  }
508
900
  } as unknown as TBase;
509
- }
510
-
511
- export type IParameterManager = InstanceType<ReturnType<typeof WithParameterManager>>;
901
+ }