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

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,27 +1,143 @@
1
1
  import {
2
- Constructor,
3
2
  isArray,
4
3
  isInstanceOf,
5
4
  isString,
6
5
  PlayerCtor,
7
- RpgCommonPlayer,
8
6
  } from "@rpgjs/common";
9
7
  import { SkillLog } from "../logs";
10
8
  import { RpgPlayer } from "./Player";
11
9
  import { Effect } from "./EffectManager";
12
10
 
13
11
  /**
14
- * Interface defining dependencies from other mixins that SkillManager needs
12
+ * Type for skill class constructor
15
13
  */
16
- interface SkillManagerDependencies {
17
- sp: number;
18
- skills(): any[];
19
- hasEffect(effect: string): boolean;
20
- databaseById(id: string): any;
21
- applyStates(player: RpgPlayer, skill: any): void;
14
+ type SkillClass = { new (...args: any[]): any };
15
+
16
+ /**
17
+ * Interface defining the hooks that can be implemented on skill classes or objects
18
+ *
19
+ * These hooks are called at specific moments during the skill lifecycle:
20
+ * - `onLearn`: When the skill is learned by the player
21
+ * - `onUse`: When the skill is successfully used
22
+ * - `onUseFailed`: When the skill usage fails (e.g., chance roll failed)
23
+ * - `onForget`: When the skill is forgotten
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const skillHooks: SkillHooks = {
28
+ * onLearn(player) {
29
+ * console.log('Skill learned!');
30
+ * },
31
+ * onUse(player, target) {
32
+ * console.log('Skill used on target');
33
+ * }
34
+ * };
35
+ * ```
36
+ */
37
+ export interface SkillHooks {
38
+ /**
39
+ * Called when the skill is learned by the player
40
+ *
41
+ * @param player - The player learning the skill
42
+ */
43
+ onLearn?: (player: RpgPlayer) => void | Promise<void>;
44
+
45
+ /**
46
+ * Called when the skill is successfully used
47
+ *
48
+ * @param player - The player using the skill
49
+ * @param target - The target player(s) if any
50
+ */
51
+ onUse?: (player: RpgPlayer, target?: RpgPlayer | RpgPlayer[]) => void | Promise<void>;
52
+
53
+ /**
54
+ * Called when the skill usage fails (e.g., chance roll failed)
55
+ *
56
+ * @param player - The player attempting to use the skill
57
+ * @param target - The intended target player(s) if any
58
+ */
59
+ onUseFailed?: (player: RpgPlayer, target?: RpgPlayer | RpgPlayer[]) => void | Promise<void>;
60
+
61
+ /**
62
+ * Called when the skill is forgotten
63
+ *
64
+ * @param player - The player forgetting the skill
65
+ */
66
+ onForget?: (player: RpgPlayer) => void | Promise<void>;
22
67
  }
23
68
 
69
+ /**
70
+ * Interface for skill object definition
71
+ *
72
+ * Defines the properties that a skill can have when defined as an object.
73
+ * Skills can be defined as objects, classes, or string IDs referencing the database.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const fireSkill: SkillObject = {
78
+ * id: 'fire',
79
+ * name: 'Fire',
80
+ * description: 'A basic fire spell',
81
+ * spCost: 10,
82
+ * hitRate: 0.9,
83
+ * power: 50,
84
+ * onUse(player) {
85
+ * console.log('Fire spell cast!');
86
+ * }
87
+ * };
88
+ *
89
+ * player.learnSkill(fireSkill);
90
+ * ```
91
+ */
92
+ export interface SkillObject extends SkillHooks {
93
+ /**
94
+ * Unique identifier for the skill
95
+ * If not provided, one will be auto-generated
96
+ */
97
+ id?: string;
98
+
99
+ /**
100
+ * Display name of the skill
101
+ */
102
+ name?: string;
103
+
104
+ /**
105
+ * Description of the skill
106
+ */
107
+ description?: string;
108
+
109
+ /**
110
+ * SP (Skill Points) cost to use the skill
111
+ * @default 0
112
+ */
113
+ spCost?: number;
114
+
115
+ /**
116
+ * Hit rate (0-1) - probability of successful skill usage
117
+ * @default 1
118
+ */
119
+ hitRate?: number;
120
+
121
+ /**
122
+ * Base power of the skill for damage calculation
123
+ */
124
+ power?: number;
125
+
126
+ /**
127
+ * Coefficient multipliers for damage calculation
128
+ */
129
+ coefficient?: Record<string, number>;
130
+
131
+ /**
132
+ * Type marker for database
133
+ */
134
+ _type?: 'skill';
24
135
 
136
+ /**
137
+ * Allow additional properties
138
+ */
139
+ [key: string]: any;
140
+ }
25
141
 
26
142
  /**
27
143
  * Skill Manager Mixin
@@ -30,92 +146,299 @@ interface SkillManagerDependencies {
30
146
  * learning, forgetting, and using skills, including SP cost management,
31
147
  * hit rate calculations, and skill effects application.
32
148
  *
149
+ * Supports three input formats for skills:
150
+ * - **String ID**: References a skill in the database
151
+ * - **Class**: A skill class that will be instantiated
152
+ * - **Object**: A skill object with properties and hooks
153
+ *
33
154
  * @param Base - The base class to extend with skill management
34
155
  * @returns Extended class with skill management methods
35
156
  *
36
157
  * @example
37
158
  * ```ts
38
- * class MyPlayer extends WithSkillManager(BasePlayer) {
39
- * constructor() {
40
- * super();
41
- * // Skill system is automatically initialized
42
- * }
43
- * }
159
+ * // Using string ID (from database)
160
+ * player.learnSkill('fire');
161
+ *
162
+ * // Using skill class
163
+ * player.learnSkill(FireSkill);
44
164
  *
45
- * const player = new MyPlayer();
46
- * player.learnSkill(Fire);
47
- * player.useSkill(Fire, targetPlayer);
165
+ * // Using skill object
166
+ * player.learnSkill({
167
+ * id: 'ice',
168
+ * name: 'Ice',
169
+ * spCost: 15,
170
+ * onUse(player) {
171
+ * console.log('Ice spell cast!');
172
+ * }
173
+ * });
48
174
  * ```
49
175
  */
50
- export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase) {
51
- return class extends Base {
52
- _getSkillIndex(skillClass: any | string) {
53
- return (this as any).skills().findIndex((skill) => {
54
- if (isString(skill)) {
55
- return skill.id == skillClass;
56
- }
57
- if (isString(skillClass)) {
58
- return skillClass == (skill.id || skill);
59
- }
60
- return isInstanceOf(skill, skillClass);
176
+ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
177
+ return class extends (Base as any) {
178
+ /**
179
+ * Find the index of a skill in the skills array
180
+ *
181
+ * Searches by ID for both string inputs and object/class inputs.
182
+ *
183
+ * @param skillInput - Skill ID, class, or object to find
184
+ * @returns Index of the skill or -1 if not found
185
+ */
186
+ _getSkillIndex(skillInput: SkillClass | SkillObject | string): number {
187
+ // Get the ID to search for
188
+ let searchId = '';
189
+
190
+ if (isString(skillInput)) {
191
+ searchId = skillInput as string;
192
+ } else if (typeof skillInput === 'function') {
193
+ // It's a class - use the class name as ID
194
+ searchId = (skillInput as any).id || skillInput.name;
195
+ } else {
196
+ // It's an object - use its id property
197
+ searchId = (skillInput as SkillObject).id || '';
198
+ }
199
+
200
+ return (this as any).skills().findIndex((skill: any) => {
201
+ const skillId = skill.id || skill.name || '';
202
+ return skillId === searchId;
61
203
  });
62
204
  }
63
205
 
64
- getSkill(skillClass: any | string) {
65
- const index = this._getSkillIndex(skillClass);
66
- return this.skills()[index] ?? null;
206
+ /**
207
+ * Retrieves a learned skill
208
+ *
209
+ * Searches the player's learned skills by ID, class, or object.
210
+ *
211
+ * @param skillInput - Skill ID, class, or object to find
212
+ * @returns The skill data if found, null otherwise
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const skill = player.getSkill('fire');
217
+ * if (skill) {
218
+ * console.log(`Fire skill costs ${skill.spCost} SP`);
219
+ * }
220
+ * ```
221
+ */
222
+ getSkill(skillInput: SkillClass | SkillObject | string): any | null {
223
+ const index = this._getSkillIndex(skillInput);
224
+ return (this as any).skills()[index] ?? null;
67
225
  }
68
226
 
69
- learnSkill(skillId: any | string) {
227
+ /**
228
+ * Learn a new skill
229
+ *
230
+ * Adds a skill to the player's skill list. Supports three input formats:
231
+ * - **String ID**: Retrieves the skill from the database
232
+ * - **Class**: Creates an instance and adds to database if needed
233
+ * - **Object**: Uses directly and adds to database if needed
234
+ *
235
+ * @param skillInput - Skill ID, class, or object to learn
236
+ * @returns The learned skill data
237
+ * @throws SkillLog.alreadyLearned if the skill is already known
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * // From database
242
+ * player.learnSkill('fire');
243
+ *
244
+ * // From class
245
+ * player.learnSkill(FireSkill);
246
+ *
247
+ * // From object
248
+ * player.learnSkill({
249
+ * id: 'custom-skill',
250
+ * name: 'Custom Skill',
251
+ * spCost: 20,
252
+ * onLearn(player) {
253
+ * console.log('Learned custom skill!');
254
+ * }
255
+ * });
256
+ * ```
257
+ */
258
+ learnSkill(skillInput: SkillClass | SkillObject | string): any {
259
+ // Get the map for database operations
260
+ const map = (this as any).getCurrentMap() || (this as any).map;
261
+
262
+ let skillId = '';
263
+ let skillData: any;
264
+
265
+ // Handle string: retrieve from database
266
+ if (isString(skillInput)) {
267
+ skillId = skillInput as string;
268
+ skillData = (this as any).databaseById(skillId);
269
+ }
270
+ // Handle class: create instance and add to database if needed
271
+ else if (typeof skillInput === 'function') {
272
+ const SkillClassCtor = skillInput as SkillClass;
273
+ skillId = (SkillClassCtor as any).id || SkillClassCtor.name;
274
+
275
+ // Check if already in database
276
+ const existingData = map?.database()?.[skillId];
277
+ if (existingData) {
278
+ skillData = existingData;
279
+ } else if (map) {
280
+ // Add the class to the database
281
+ map.addInDatabase(skillId, SkillClassCtor);
282
+ skillData = SkillClassCtor;
283
+ } else {
284
+ skillData = SkillClassCtor;
285
+ }
286
+
287
+ // Create instance of the class for hooks
288
+ const skillInstance = new SkillClassCtor();
289
+ // Merge instance properties with class static properties
290
+ skillData = { ...skillData, ...skillInstance, id: skillId };
291
+ }
292
+ // Handle object: use directly and add to database if needed
293
+ else {
294
+ const skillObj = skillInput as SkillObject;
295
+ skillId = skillObj.id || `skill-${Date.now()}`;
296
+
297
+ // Ensure the object has an id
298
+ skillObj.id = skillId;
299
+
300
+ // Check if already in database
301
+ const existingData = map?.database()?.[skillId];
302
+ if (existingData) {
303
+ // Merge with existing data
304
+ skillData = { ...existingData, ...skillObj };
305
+ if (map) {
306
+ map.addInDatabase(skillId, skillData, { force: true });
307
+ }
308
+ } else if (map) {
309
+ // Add the object to the database
310
+ map.addInDatabase(skillId, skillObj);
311
+ skillData = skillObj;
312
+ } else {
313
+ skillData = skillObj;
314
+ }
315
+ }
316
+
317
+ // Check if already learned
70
318
  if (this.getSkill(skillId)) {
71
- throw SkillLog.alreadyLearned(skillId);
319
+ throw SkillLog.alreadyLearned(skillData);
72
320
  }
73
- const instance = (this as any).databaseById(skillId);
74
- this.skills().push(instance);
75
- this["execMethod"]("onLearn", [this], instance);
76
- return instance;
321
+
322
+ // Add to skills list
323
+ (this as any).skills().push(skillData);
324
+
325
+ // Call onLearn hook
326
+ this["execMethod"]("onLearn", [this], skillData);
327
+
328
+ return skillData;
77
329
  }
78
330
 
79
- forgetSkill(skillId: any | string) {
80
- if (isString(skillId)) skillId = (this as any).databaseById(skillId);
81
- const index = this._getSkillIndex(skillId);
82
- if (index == -1) {
83
- throw SkillLog.notLearned(skillId);
331
+ /**
332
+ * Forget a learned skill
333
+ *
334
+ * Removes a skill from the player's skill list.
335
+ *
336
+ * @param skillInput - Skill ID, class, or object to forget
337
+ * @returns The forgotten skill data
338
+ * @throws SkillLog.notLearned if the skill is not known
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * player.forgetSkill('fire');
343
+ * // or
344
+ * player.forgetSkill(FireSkill);
345
+ * ```
346
+ */
347
+ forgetSkill(skillInput: SkillClass | SkillObject | string): any {
348
+ const index = this._getSkillIndex(skillInput);
349
+
350
+ if (index === -1) {
351
+ // Get skill data for error message
352
+ let skillData: any = skillInput;
353
+ if (isString(skillInput)) {
354
+ try {
355
+ skillData = (this as any).databaseById(skillInput);
356
+ } catch {
357
+ skillData = { name: skillInput, id: skillInput };
358
+ }
359
+ } else if (typeof skillInput === 'function') {
360
+ skillData = { name: (skillInput as SkillClass).name, id: (skillInput as any).id || (skillInput as SkillClass).name };
361
+ }
362
+ throw SkillLog.notLearned(skillData);
84
363
  }
85
- const instance = this.skills()[index];
86
- this.skills().splice(index, 1);
87
- this["execMethod"]("onForget", [this], instance);
88
- return instance;
364
+
365
+ const skillData = (this as any).skills()[index];
366
+ (this as any).skills().splice(index, 1);
367
+
368
+ // Call onForget hook
369
+ this["execMethod"]("onForget", [this], skillData);
370
+
371
+ return skillData;
89
372
  }
90
373
 
91
- useSkill(skillId: any | string, otherPlayer?: RpgPlayer | RpgPlayer[]) {
92
- const skill = this.getSkill(skillId);
374
+ /**
375
+ * Use a learned skill
376
+ *
377
+ * Executes a skill, consuming SP and applying effects to targets.
378
+ * The skill must be learned and the player must have enough SP.
379
+ *
380
+ * @param skillInput - Skill ID, class, or object to use
381
+ * @param otherPlayer - Optional target player(s) to apply skill effects to
382
+ * @returns The used skill data
383
+ * @throws SkillLog.restriction if player has CAN_NOT_SKILL effect
384
+ * @throws SkillLog.notLearned if skill is not known
385
+ * @throws SkillLog.notEnoughSp if not enough SP
386
+ * @throws SkillLog.chanceToUseFailed if hit rate check fails
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * // Use skill without target
391
+ * player.useSkill('fire');
392
+ *
393
+ * // Use skill on a target
394
+ * player.useSkill('fire', enemy);
395
+ *
396
+ * // Use skill on multiple targets
397
+ * player.useSkill('fire', [enemy1, enemy2]);
398
+ * ```
399
+ */
400
+ useSkill(skillInput: SkillClass | SkillObject | string, otherPlayer?: RpgPlayer | RpgPlayer[]): any {
401
+ const skill = this.getSkill(skillInput);
402
+
403
+ // Check for skill restriction effect
93
404
  if ((this as any).hasEffect(Effect.CAN_NOT_SKILL)) {
94
- throw SkillLog.restriction(skillId);
405
+ throw SkillLog.restriction(skill || skillInput);
95
406
  }
407
+
408
+ // Check if skill is learned
96
409
  if (!skill) {
97
- throw SkillLog.notLearned(skillId);
410
+ throw SkillLog.notLearned(skillInput);
98
411
  }
99
- if (skill.spCost > (this as any).sp) {
100
- throw SkillLog.notEnoughSp(skillId, skill.spCost, (this as any).sp);
412
+
413
+ // Check SP cost
414
+ const spCost = skill.spCost || 0;
415
+ if (spCost > (this as any).sp) {
416
+ throw SkillLog.notEnoughSp(skill, spCost, (this as any).sp);
101
417
  }
102
- (this as any).sp -= skill.spCost / ((this as any).hasEffect(Effect.HALF_SP_COST) ? 2 : 1);
418
+
419
+ // Consume SP (halved if HALF_SP_COST effect is active)
420
+ const costMultiplier = (this as any).hasEffect(Effect.HALF_SP_COST) ? 2 : 1;
421
+ (this as any).sp -= spCost / costMultiplier;
422
+
423
+ // Check hit rate
103
424
  const hitRate = skill.hitRate ?? 1;
104
425
  if (Math.random() > hitRate) {
105
426
  this["execMethod"]("onUseFailed", [this, otherPlayer], skill);
106
- throw SkillLog.chanceToUseFailed(skillId);
427
+ throw SkillLog.chanceToUseFailed(skill);
107
428
  }
429
+
430
+ // Apply effects to targets
108
431
  if (otherPlayer) {
109
- let players: any = otherPlayer;
110
- if (!isArray(players)) {
111
- players = [otherPlayer];
112
- }
113
- for (let player of players) {
432
+ const players: RpgPlayer[] = isArray(otherPlayer) ? otherPlayer as RpgPlayer[] : [otherPlayer as RpgPlayer];
433
+ for (const player of players) {
114
434
  (this as any).applyStates(player, skill);
115
435
  (player as any).applyDamage(this, skill);
116
436
  }
117
437
  }
438
+
439
+ // Call onUse hook
118
440
  this["execMethod"]("onUse", [this, otherPlayer], skill);
441
+
119
442
  return skill;
120
443
  }
121
444
  } as unknown as TBase;
@@ -131,39 +454,44 @@ export interface ISkillManager {
131
454
  /**
132
455
  * Retrieves a learned skill. Returns null if not found
133
456
  *
134
- * @param skillClass - Skill class or data id
135
- * @returns Instance of SkillClass or null
457
+ * @param skillInput - Skill class, object, or data id
458
+ * @returns The skill data or null
136
459
  */
137
- getSkill(skillClass: any | string): any | null;
460
+ getSkill(skillInput: SkillClass | SkillObject | string): any | null;
138
461
 
139
462
  /**
140
463
  * Learn a skill
141
464
  *
142
- * @param skillId - Skill class or data id
143
- * @returns Instance of SkillClass
465
+ * Supports three input formats:
466
+ * - String ID: Retrieves from database
467
+ * - Class: Creates instance and adds to database
468
+ * - Object: Uses directly and adds to database
469
+ *
470
+ * @param skillInput - Skill class, object, or data id
471
+ * @returns The learned skill data
144
472
  * @throws SkillLog.alreadyLearned if the player already knows the skill
145
473
  */
146
- learnSkill(skillId: any | string): any;
474
+ learnSkill(skillInput: SkillClass | SkillObject | string): any;
147
475
 
148
476
  /**
149
477
  * Forget a skill
150
478
  *
151
- * @param skillId - Skill class or data id
152
- * @returns Instance of SkillClass
479
+ * @param skillInput - Skill class, object, or data id
480
+ * @returns The forgotten skill data
153
481
  * @throws SkillLog.notLearned if trying to forget a skill not learned
154
482
  */
155
- forgetSkill(skillId: any | string): any;
483
+ forgetSkill(skillInput: SkillClass | SkillObject | string): any;
156
484
 
157
485
  /**
158
- * Using a skill
486
+ * Use a skill
159
487
  *
160
- * @param skillId - Skill class or data id
488
+ * @param skillInput - Skill class, object, or data id
161
489
  * @param otherPlayer - Optional target player(s) to apply skill to
162
- * @returns Instance of SkillClass
490
+ * @returns The used skill data
163
491
  * @throws SkillLog.restriction if player has Effect.CAN_NOT_SKILL
164
492
  * @throws SkillLog.notLearned if player tries to use an unlearned skill
165
493
  * @throws SkillLog.notEnoughSp if player does not have enough SP
166
494
  * @throws SkillLog.chanceToUseFailed if the chance to use the skill has failed
167
495
  */
168
- useSkill(skillId: any | string, otherPlayer?: RpgPlayer | RpgPlayer[]): any;
496
+ useSkill(skillInput: SkillClass | SkillObject | string, otherPlayer?: RpgPlayer | RpgPlayer[]): any;
169
497
  }
package/src/rooms/map.ts CHANGED
@@ -281,11 +281,24 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
281
281
  const activeCollisions = new Set<string>();
282
282
  const activeShapeCollisions = new Set<string>();
283
283
 
284
+ // Helper function to check if entities have different z (height)
285
+ const hasDifferentZ = (entityA: any, entityB: any): boolean => {
286
+ const zA = entityA.owner.z();
287
+ const zB = entityB.owner.z();
288
+ return zA !== zB;
289
+ };
290
+
284
291
  // Listen to collision enter events
285
292
  this.physic.getEvents().onCollisionEnter((collision) => {
286
293
  const entityA = collision.entityA;
287
294
  const entityB = collision.entityB;
288
295
 
296
+ // Skip collision callbacks if entities have different z (height)
297
+ // Higher z entities should not trigger collision callbacks with lower z entities
298
+ if (hasDifferentZ(entityA, entityB)) {
299
+ return;
300
+ }
301
+
289
302
  // Create a unique key for this collision pair
290
303
  const collisionKey = entityA.uuid < entityB.uuid
291
304
  ? `${entityA.uuid}-${entityB.uuid}`
@@ -351,6 +364,11 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
351
364
  const entityA = collision.entityA;
352
365
  const entityB = collision.entityB;
353
366
 
367
+ // Skip collision callbacks if entities have different z (height)
368
+ if (hasDifferentZ(entityA, entityB)) {
369
+ return;
370
+ }
371
+
354
372
  const collisionKey = entityA.uuid < entityB.uuid
355
373
  ? `${entityA.uuid}-${entityB.uuid}`
356
374
  : `${entityB.uuid}-${entityA.uuid}`;