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

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.
Files changed (51) hide show
  1. package/dist/Gui/DialogGui.d.ts +1 -0
  2. package/dist/Gui/GameoverGui.d.ts +23 -0
  3. package/dist/Gui/Gui.d.ts +6 -0
  4. package/dist/Gui/MenuGui.d.ts +22 -3
  5. package/dist/Gui/NotificationGui.d.ts +1 -2
  6. package/dist/Gui/SaveLoadGui.d.ts +13 -0
  7. package/dist/Gui/ShopGui.d.ts +24 -3
  8. package/dist/Gui/TitleGui.d.ts +23 -0
  9. package/dist/Gui/index.d.ts +9 -1
  10. package/dist/Player/GuiManager.d.ts +86 -3
  11. package/dist/Player/Player.d.ts +24 -2
  12. package/dist/RpgServer.d.ts +7 -0
  13. package/dist/RpgServerEngine.d.ts +2 -1
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.js +1621 -336
  16. package/dist/index.js.map +1 -1
  17. package/dist/presets/index.d.ts +0 -9
  18. package/dist/rooms/BaseRoom.d.ts +37 -0
  19. package/dist/rooms/lobby.d.ts +6 -1
  20. package/dist/rooms/map.d.ts +22 -1
  21. package/dist/services/save.d.ts +43 -0
  22. package/dist/storage/index.d.ts +1 -0
  23. package/dist/storage/localStorage.d.ts +23 -0
  24. package/package.json +10 -10
  25. package/src/Gui/DialogGui.ts +12 -2
  26. package/src/Gui/GameoverGui.ts +39 -0
  27. package/src/Gui/Gui.ts +23 -1
  28. package/src/Gui/MenuGui.ts +155 -6
  29. package/src/Gui/NotificationGui.ts +1 -2
  30. package/src/Gui/SaveLoadGui.ts +60 -0
  31. package/src/Gui/ShopGui.ts +145 -16
  32. package/src/Gui/TitleGui.ts +39 -0
  33. package/src/Gui/index.ts +13 -2
  34. package/src/Player/BattleManager.ts +1 -1
  35. package/src/Player/ClassManager.ts +57 -2
  36. package/src/Player/GuiManager.ts +125 -14
  37. package/src/Player/ItemManager.ts +160 -41
  38. package/src/Player/ParameterManager.ts +1 -1
  39. package/src/Player/Player.ts +87 -12
  40. package/src/Player/SkillManager.ts +145 -66
  41. package/src/Player/StateManager.ts +70 -1
  42. package/src/Player/VariableManager.ts +10 -7
  43. package/src/RpgServer.ts +8 -0
  44. package/src/index.ts +5 -2
  45. package/src/presets/index.ts +1 -10
  46. package/src/rooms/BaseRoom.ts +112 -0
  47. package/src/rooms/lobby.ts +13 -6
  48. package/src/rooms/map.ts +31 -4
  49. package/src/services/save.ts +147 -0
  50. package/src/storage/index.ts +1 -0
  51. package/src/storage/localStorage.ts +76 -0
@@ -1,5 +1,5 @@
1
1
  import { isInstanceOf, isString, Item, type PlayerCtor} from "@rpgjs/common";
2
- import { ATK, PDEF, SDEF } from "../presets";
2
+ import { ATK, PDEF, SDEF } from "@rpgjs/common";
3
3
  import { ItemLog } from "../logs";
4
4
  import type { ItemClass, ItemInstance } from "@rpgjs/database";
5
5
  import { RpgPlayer } from "./Player";
@@ -205,64 +205,196 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
205
205
  return isInstanceOf(it, itemClass);
206
206
  });
207
207
  }
208
- addItem(item: ItemClass | ItemObject | string, nb: number = 1): Item {
208
+
209
+ private _getItemMap(required: boolean = true) {
209
210
  // 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
+ const map = (this as any).getCurrentMap?.() || (this as any).map;
212
+ if (required && (!map || !map.database)) {
213
213
  throw new Error('Player must be on a map to add items');
214
214
  }
215
+ return map;
216
+ }
215
217
 
218
+ private _resolveItemInput(
219
+ item: ItemClass | ItemObject | string,
220
+ map: any,
221
+ databaseByIdOverride?: (id: string) => any
222
+ ) {
216
223
  let itemId: string;
217
224
  let data: any;
218
225
  let itemInstance: any = null;
219
226
 
220
- // Handle string: retrieve from database
221
227
  if (isString(item)) {
222
228
  itemId = item as string;
223
- data = (this as any).databaseById(itemId);
224
- }
225
- // Handle class: create instance and add to database if needed
226
- else if (typeof item === 'function' || (item as any).prototype) {
229
+ data = databaseByIdOverride
230
+ ? databaseByIdOverride(itemId)
231
+ : (this as any).databaseById(itemId);
232
+ } else if (typeof item === 'function' || (item as any).prototype) {
227
233
  itemId = (item as any).name;
228
-
229
- // Check if already in database
234
+
230
235
  const existingData = map.database()[itemId];
231
236
  if (existingData) {
232
- // Use existing data from database
233
237
  data = existingData;
234
238
  } else {
235
- // Add the class to the database (it will be retrieved later via databaseById)
236
239
  map.addInDatabase(itemId, item as ItemClass);
237
- // Use the class as data (it will be used to create Item instance)
238
240
  data = item as ItemClass;
239
241
  }
240
-
241
- // Create instance of the class for hooks
242
+
242
243
  itemInstance = new (item as ItemClass)();
243
- }
244
- // Handle object: use directly and add to database if needed
245
- else {
244
+ } else {
246
245
  const itemObj = item as ItemObject;
247
246
  itemId = itemObj.id || `item-${Date.now()}`;
248
-
249
- // Check if already in database
247
+
250
248
  const existingData = map.database()[itemId];
251
249
  if (existingData) {
252
- // Merge with existing data and force update
253
250
  data = { ...existingData, ...itemObj };
254
- // Update database with merged data (force overwrite)
255
251
  map.addInDatabase(itemId, data, { force: true });
256
252
  } else {
257
- // Add the object to the database
258
253
  map.addInDatabase(itemId, itemObj);
259
- // Use object directly as data
260
254
  data = itemObj;
261
255
  }
262
-
256
+
263
257
  itemInstance = itemObj;
264
258
  }
265
259
 
260
+ return { itemId, data, itemInstance };
261
+ }
262
+
263
+ private _createItemInstance(
264
+ itemId: string,
265
+ data: any,
266
+ nb: number,
267
+ itemInstance: any
268
+ ): Item {
269
+ const instance = new Item(data);
270
+ instance.id.set(itemId);
271
+ instance.quantity.set(nb);
272
+
273
+ if (itemInstance) {
274
+ (instance as any)._itemInstance = itemInstance;
275
+ if (itemInstance.onAdd) {
276
+ instance.onAdd = itemInstance.onAdd.bind(itemInstance);
277
+ }
278
+ }
279
+
280
+ return instance;
281
+ }
282
+
283
+ /**
284
+ * Create an item instance without inventory changes or hook execution.
285
+ */
286
+ createItemInstance(item: ItemClass | ItemObject | string, nb: number = 1) {
287
+ const map = this._getItemMap();
288
+ const { itemId, data, itemInstance } = this._resolveItemInput(item, map);
289
+ const instance = this._createItemInstance(itemId, data, nb, itemInstance);
290
+ return { itemId, data, itemInstance, instance };
291
+ }
292
+
293
+ /**
294
+ * Resolve item snapshot entries into Item instances without side effects.
295
+ */
296
+ resolveItemsSnapshot(snapshot: { items?: any[] }, mapOverride?: any) {
297
+ if (!snapshot || !Array.isArray(snapshot.items)) {
298
+ return snapshot;
299
+ }
300
+
301
+ const map = mapOverride ?? this._getItemMap(false);
302
+ if (!map || !map.database) {
303
+ return snapshot;
304
+ }
305
+
306
+ const databaseByIdOverride = (id: string) => {
307
+ const data = map.database()[id];
308
+ if (!data) {
309
+ throw new Error(
310
+ `The ID=${id} data is not found in the database. Add the data in the property "database"`
311
+ );
312
+ }
313
+ return data;
314
+ };
315
+
316
+ const items = snapshot.items.map((entry: any) => {
317
+ const itemId = isString(entry) ? entry : entry?.id;
318
+ if (!itemId) {
319
+ return entry;
320
+ }
321
+
322
+ const nb =
323
+ !isString(entry) && typeof entry?.nb === 'number'
324
+ ? entry.nb
325
+ : !isString(entry) && typeof entry?.quantity === 'number'
326
+ ? entry.quantity
327
+ : 1;
328
+
329
+ const { data, itemInstance } = this._resolveItemInput(
330
+ itemId,
331
+ map,
332
+ databaseByIdOverride
333
+ );
334
+ return this._createItemInstance(itemId, data, nb, itemInstance);
335
+ });
336
+
337
+ return { ...snapshot, items };
338
+ }
339
+
340
+ /**
341
+ * Resolve equipment snapshot entries into Item instances without side effects.
342
+ */
343
+ resolveEquipmentsSnapshot(snapshot: { equipments?: any[]; items?: any[] }, mapOverride?: any) {
344
+ if (!snapshot || !Array.isArray(snapshot.equipments)) {
345
+ return snapshot;
346
+ }
347
+
348
+ const map = mapOverride ?? this._getItemMap(false);
349
+ if (!map || !map.database) {
350
+ return snapshot;
351
+ }
352
+
353
+ const databaseByIdOverride = (id: string) => {
354
+ const data = map.database()[id];
355
+ if (!data) {
356
+ throw new Error(
357
+ `The ID=${id} data is not found in the database. Add the data in the property "database"`
358
+ );
359
+ }
360
+ return data;
361
+ };
362
+
363
+ const resolvedItems = Array.isArray(snapshot.items) ? snapshot.items : [];
364
+ const getItemId = (entry: any) => {
365
+ if (isString(entry)) return entry;
366
+ if (typeof entry?.id === 'function') return entry.id();
367
+ return entry?.id;
368
+ };
369
+
370
+ const equipments = snapshot.equipments.map((entry: any) => {
371
+ const itemId = getItemId(entry);
372
+ if (!itemId) {
373
+ return entry;
374
+ }
375
+
376
+ const existing = resolvedItems.find((item: any) => {
377
+ const existingId = getItemId(item);
378
+ return existingId === itemId;
379
+ });
380
+ if (existing) {
381
+ return existing;
382
+ }
383
+
384
+ const { data, itemInstance } = this._resolveItemInput(
385
+ itemId,
386
+ map,
387
+ databaseByIdOverride
388
+ );
389
+ return this._createItemInstance(itemId, data, 1, itemInstance);
390
+ });
391
+
392
+ return { ...snapshot, equipments };
393
+ }
394
+ addItem(item: ItemClass | ItemObject | string, nb: number = 1): Item {
395
+ const map = this._getItemMap();
396
+ const { itemId, data, itemInstance } = this._resolveItemInput(item, map);
397
+
266
398
  // Find existing item in inventory
267
399
  const existingItem = (this as any).items().find((it: Item) => it.id() == itemId);
268
400
  let instance: Item;
@@ -293,20 +425,7 @@ export function WithItemManager<TBase extends PlayerCtor>(Base: TBase) {
293
425
  }
294
426
  } else {
295
427
  // Create new item instance
296
- instance = new Item(data);
297
- instance.id.set(itemId);
298
- instance.quantity.set(nb);
299
-
300
- // Attach hooks from class instance or object
301
- if (itemInstance) {
302
- // Store the original instance for hook execution
303
- (instance as any)._itemInstance = itemInstance;
304
-
305
- // Attach onAdd hook directly for immediate use
306
- if (itemInstance.onAdd) {
307
- instance.onAdd = itemInstance.onAdd.bind(itemInstance);
308
- }
309
- };
428
+ instance = this._createItemInstance(itemId, data, nb, itemInstance);
310
429
  (this as any).items().push(instance);
311
430
  }
312
431
 
@@ -1,6 +1,6 @@
1
1
  import { isString, PlayerCtor } from "@rpgjs/common";
2
2
  import { signal, computed, WritableSignal, ComputedSignal } from "@signe/reactive";
3
- import { MAXHP, MAXSP } from "../presets";
3
+ import { MAXHP, MAXSP } from "@rpgjs/common";
4
4
  import { sync, type } from "@signe/sync";
5
5
 
6
6
  /**
@@ -19,7 +19,7 @@ import { MockConnection } from "@signe/room";
19
19
  import { IMoveManager, WithMoveManager } from "./MoveManager";
20
20
  import { IGoldManager, WithGoldManager } from "./GoldManager";
21
21
  import { WithVariableManager, type IVariableManager } from "./VariableManager";
22
- import { createStatesSnapshot, load, sync, type } from "@signe/sync";
22
+ import { createStatesSnapshotDeep, load, sync, type } from "@signe/sync";
23
23
  import { computed, signal } from "@signe/reactive";
24
24
  import {
25
25
  IParameterManager,
@@ -29,12 +29,23 @@ import { WithItemFixture } from "./ItemFixture";
29
29
  import { IItemManager, WithItemManager } from "./ItemManager";
30
30
  import { bufferTime, combineLatest, debounceTime, distinctUntilChanged, filter, lastValueFrom, map, Observable, pairwise, sample, throttleTime } from "rxjs";
31
31
  import { IEffectManager, WithEffectManager } from "./EffectManager";
32
- import { AGI, AGI_CURVE, DEX, DEX_CURVE, INT, INT_CURVE, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, STR, STR_CURVE } from "../presets";
32
+ import { AGI, DEX, INT, MAXHP, MAXSP, STR } from "@rpgjs/common";
33
+ import { AGI_CURVE, DEX_CURVE, INT_CURVE, MAXHP_CURVE, MAXSP_CURVE, STR_CURVE } from "../presets";
33
34
  import { IElementManager, WithElementManager } from "./ElementManager";
34
35
  import { ISkillManager, WithSkillManager } from "./SkillManager";
35
36
  import { IBattleManager, WithBattleManager } from "./BattleManager";
36
37
  import { IClassManager, WithClassManager } from "./ClassManager";
37
38
  import { IStateManager, WithStateManager } from "./StateManager";
39
+ import {
40
+ buildSaveSlotMeta,
41
+ resolveAutoSaveStrategy,
42
+ resolveSaveSlot,
43
+ resolveSaveStorageStrategy,
44
+ shouldAutoSave,
45
+ type SaveRequestContext,
46
+ type SaveSlotIndex,
47
+ type SaveSlotMeta,
48
+ } from "../services/save";
38
49
 
39
50
  /**
40
51
  * Combines multiple RpgCommonPlayer mixins into one
@@ -403,17 +414,78 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
403
414
  });
404
415
  }
405
416
 
406
- async save() {
407
- const snapshot = createStatesSnapshot(this)
408
- await lastValueFrom(this.hooks.callHooks("server-player-onSave", this, snapshot))
409
- return JSON.stringify(snapshot)
417
+ snapshot() {
418
+ return createStatesSnapshotDeep(this);
410
419
  }
411
420
 
412
- async load(snapshot: string) {
413
- const data = JSON.parse(snapshot)
414
- const dataLoaded = load(this, data)
415
- await lastValueFrom(this.hooks.callHooks("server-player-onLoad", this, dataLoaded))
416
- return dataLoaded
421
+ async applySnapshot(snapshot: string | object) {
422
+ const data = typeof snapshot === "string" ? JSON.parse(snapshot) : snapshot;
423
+ const withItems = (this as any).resolveItemsSnapshot?.(data) ?? data;
424
+ const withSkills = (this as any).resolveSkillsSnapshot?.(withItems) ?? withItems;
425
+ const withStates = (this as any).resolveStatesSnapshot?.(withSkills) ?? withSkills;
426
+ const withClass = (this as any).resolveClassSnapshot?.(withStates) ?? withStates;
427
+ const resolvedSnapshot = (this as any).resolveEquipmentsSnapshot?.(withClass) ?? withClass;
428
+ load(this, resolvedSnapshot);
429
+ if (Array.isArray(resolvedSnapshot.items)) {
430
+ this.items.set(resolvedSnapshot.items);
431
+ }
432
+ if (Array.isArray(resolvedSnapshot.skills)) {
433
+ this.skills.set(resolvedSnapshot.skills);
434
+ }
435
+ if (Array.isArray(resolvedSnapshot.states)) {
436
+ this.states.set(resolvedSnapshot.states);
437
+ }
438
+ if (resolvedSnapshot._class != null && this._class?.set) {
439
+ this._class.set(resolvedSnapshot._class);
440
+ }
441
+ if (Array.isArray(resolvedSnapshot.equipments)) {
442
+ this.equipments.set(resolvedSnapshot.equipments);
443
+ }
444
+ await lastValueFrom(this.hooks.callHooks("server-player-onLoad", this, resolvedSnapshot));
445
+ return resolvedSnapshot;
446
+ }
447
+
448
+ async save(slot: SaveSlotIndex = "auto", meta: SaveSlotMeta = {}, context: SaveRequestContext = {}) {
449
+ const policy = resolveAutoSaveStrategy();
450
+ if (policy.canSave && !policy.canSave(this, context)) {
451
+ return null;
452
+ }
453
+ const resolvedSlot = resolveSaveSlot(slot, policy, this, context);
454
+ if (resolvedSlot === null) {
455
+ return null;
456
+ }
457
+ const snapshot = this.snapshot();
458
+ await lastValueFrom(this.hooks.callHooks("server-player-onSave", this, snapshot));
459
+ const storage = resolveSaveStorageStrategy();
460
+ const finalMeta = buildSaveSlotMeta(this, meta);
461
+ await storage.save(this, resolvedSlot, JSON.stringify(snapshot), finalMeta);
462
+ return { index: resolvedSlot, meta: finalMeta };
463
+ }
464
+
465
+ async load(
466
+ slot: SaveSlotIndex = "auto",
467
+ context: SaveRequestContext = {},
468
+ options: { changeMap?: boolean } = {}
469
+ ) {
470
+ const policy = resolveAutoSaveStrategy();
471
+ if (policy.canLoad && !policy.canLoad(this, context)) {
472
+ return { ok: false };
473
+ }
474
+ const resolvedSlot = resolveSaveSlot(slot, policy, this, context);
475
+ if (resolvedSlot === null) {
476
+ return { ok: false };
477
+ }
478
+ const storage = resolveSaveStorageStrategy();
479
+ const slotData = await storage.get(this, resolvedSlot);
480
+ if (!slotData?.snapshot) {
481
+ return { ok: false };
482
+ }
483
+ await this.applySnapshot(slotData.snapshot);
484
+ const { snapshot, ...meta } = slotData;
485
+ if (options.changeMap !== false && meta.map) {
486
+ await this.changeMap(meta.map);
487
+ }
488
+ return { ok: true, slot: meta, index: resolvedSlot };
417
489
  }
418
490
 
419
491
 
@@ -531,6 +603,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
531
603
  */
532
604
  syncChanges() {
533
605
  this._eventChanges();
606
+ if (shouldAutoSave(this, { reason: "auto", source: "syncChanges" })) {
607
+ void this.save("auto", {}, { reason: "auto", source: "syncChanges" });
608
+ }
534
609
  }
535
610
 
536
611
  databaseById(id: string) {
@@ -1234,4 +1309,4 @@ export interface RpgPlayer extends
1234
1309
  ISkillManager,
1235
1310
  IBattleManager,
1236
1311
  IClassManager,
1237
- IStateManager { }
1312
+ IStateManager { }
@@ -3,6 +3,7 @@ import {
3
3
  isInstanceOf,
4
4
  isString,
5
5
  PlayerCtor,
6
+ Skill,
6
7
  } from "@rpgjs/common";
7
8
  import { SkillLog } from "../logs";
8
9
  import { RpgPlayer } from "./Player";
@@ -175,6 +176,134 @@ export interface SkillObject extends SkillHooks {
175
176
  */
176
177
  export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
177
178
  return class extends (Base as any) {
179
+ private _getSkillMap(required: boolean = true) {
180
+ // Use this.map directly to support both RpgMap and LobbyRoom
181
+ const map = (this as any).getCurrentMap?.() || (this as any).map;
182
+ if (required && (!map || !map.database)) {
183
+ throw new Error('Player must be on a map to learn skills');
184
+ }
185
+ return map;
186
+ }
187
+
188
+ private _resolveSkillInput(
189
+ skillInput: SkillClass | SkillObject | string,
190
+ map: any,
191
+ databaseByIdOverride?: (id: string) => any
192
+ ) {
193
+ let skillId = '';
194
+ let skillData: any;
195
+ let skillInstance: any = null;
196
+
197
+ if (isString(skillInput)) {
198
+ skillId = skillInput as string;
199
+ skillData = databaseByIdOverride
200
+ ? databaseByIdOverride(skillId)
201
+ : (this as any).databaseById(skillId);
202
+ } else if (typeof skillInput === 'function') {
203
+ const SkillClassCtor = skillInput as SkillClass;
204
+ skillId = (SkillClassCtor as any).id || SkillClassCtor.name;
205
+
206
+ const existingData = map?.database()?.[skillId];
207
+ if (existingData) {
208
+ skillData = existingData;
209
+ } else if (map) {
210
+ map.addInDatabase(skillId, SkillClassCtor);
211
+ skillData = SkillClassCtor;
212
+ } else {
213
+ skillData = SkillClassCtor;
214
+ }
215
+
216
+ skillInstance = new SkillClassCtor();
217
+ skillData = { ...skillData, ...skillInstance, id: skillId };
218
+ } else {
219
+ const skillObj = skillInput as SkillObject;
220
+ skillId = skillObj.id || `skill-${Date.now()}`;
221
+ skillObj.id = skillId;
222
+
223
+ const existingData = map?.database()?.[skillId];
224
+ if (existingData) {
225
+ skillData = { ...existingData, ...skillObj };
226
+ if (map) {
227
+ map.addInDatabase(skillId, skillData, { force: true });
228
+ }
229
+ } else if (map) {
230
+ map.addInDatabase(skillId, skillObj);
231
+ skillData = skillObj;
232
+ } else {
233
+ skillData = skillObj;
234
+ }
235
+
236
+ skillInstance = skillObj;
237
+ }
238
+
239
+ return { skillId, skillData, skillInstance };
240
+ }
241
+
242
+ private _createSkillInstance(
243
+ skillId: string,
244
+ skillData: any,
245
+ skillInstance: any
246
+ ) {
247
+ const instance = new Skill(skillData);
248
+ instance.id.set(skillId);
249
+
250
+ if (skillInstance) {
251
+ (instance as any)._skillInstance = skillInstance;
252
+ }
253
+
254
+ return instance;
255
+ }
256
+
257
+ /**
258
+ * Create a skill instance without learning side effects.
259
+ */
260
+ createSkillInstance(skillInput: SkillClass | SkillObject | string) {
261
+ const map = this._getSkillMap();
262
+ const { skillId, skillData, skillInstance } = this._resolveSkillInput(skillInput, map);
263
+ const instance = this._createSkillInstance(skillId, skillData, skillInstance);
264
+ return { skillId, skillData, skillInstance, instance };
265
+ }
266
+
267
+ /**
268
+ * Resolve skill snapshot entries into Skill instances without side effects.
269
+ */
270
+ resolveSkillsSnapshot(snapshot: { skills?: any[] }, mapOverride?: any) {
271
+ if (!snapshot || !Array.isArray(snapshot.skills)) {
272
+ return snapshot;
273
+ }
274
+
275
+ const map = mapOverride ?? this._getSkillMap(false);
276
+ if (!map || !map.database) {
277
+ return snapshot;
278
+ }
279
+
280
+ const databaseByIdOverride = (id: string) => {
281
+ const data = map.database()[id];
282
+ if (!data) {
283
+ throw new Error(
284
+ `The ID=${id} data is not found in the database. Add the data in the property "database"`
285
+ );
286
+ }
287
+ return data;
288
+ };
289
+
290
+ const skills = snapshot.skills.map((entry: any) => {
291
+ const skillId = isString(entry) ? entry : entry?.id;
292
+ if (!skillId) {
293
+ return entry;
294
+ }
295
+
296
+ const { skillData, skillInstance } = this._resolveSkillInput(
297
+ skillId,
298
+ map,
299
+ databaseByIdOverride
300
+ );
301
+ return this._createSkillInstance(skillId, skillData, skillInstance);
302
+ });
303
+
304
+ return { ...snapshot, skills };
305
+ }
306
+
178
307
  /**
179
308
  * Find the index of a skill in the skills array
180
309
  *
@@ -198,7 +327,7 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
198
327
  }
199
328
 
200
329
  return (this as any).skills().findIndex((skill: any) => {
201
- const skillId = skill.id || skill.name || '';
330
+ const skillId = skill.id() || skill.name() || '';
202
331
  return skillId === searchId;
203
332
  });
204
333
  }
@@ -219,9 +348,9 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
219
348
  * }
220
349
  * ```
221
350
  */
222
- getSkill(skillInput: SkillClass | SkillObject | string): any | null {
351
+ getSkill(skillInput: SkillClass | SkillObject | string): Skill | null {
223
352
  const index = this._getSkillIndex(skillInput);
224
- return (this as any).skills()[index] ?? null;
353
+ return (this as any).skills()[index] as Skill ?? null;
225
354
  }
226
355
 
227
356
  /**
@@ -256,74 +385,21 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
256
385
  * ```
257
386
  */
258
387
  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
- }
388
+ const map = this._getSkillMap();
389
+ const { skillId, skillData, skillInstance } = this._resolveSkillInput(skillInput, map);
316
390
 
317
391
  // Check if already learned
318
392
  if (this.getSkill(skillId)) {
319
393
  throw SkillLog.alreadyLearned(skillData);
320
394
  }
321
395
 
322
- // Add to skills list
323
- (this as any).skills().push(skillData);
396
+ const instance = this._createSkillInstance(skillId, skillData, skillInstance);
397
+
398
+ (this as any).skills().push(instance);
324
399
 
325
400
  // Call onLearn hook
326
- this["execMethod"]("onLearn", [this], skillData);
401
+ const hookTarget = (instance as any)._skillInstance || instance;
402
+ this["execMethod"]("onLearn", [this], hookTarget);
327
403
 
328
404
  return skillData;
329
405
  }
@@ -366,7 +442,8 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
366
442
  (this as any).skills().splice(index, 1);
367
443
 
368
444
  // Call onForget hook
369
- this["execMethod"]("onForget", [this], skillData);
445
+ const hookTarget = (skillData as any)?._skillInstance || skillData;
446
+ this["execMethod"]("onForget", [this], hookTarget);
370
447
 
371
448
  return skillData;
372
449
  }
@@ -423,7 +500,8 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
423
500
  // Check hit rate
424
501
  const hitRate = skill.hitRate ?? 1;
425
502
  if (Math.random() > hitRate) {
426
- this["execMethod"]("onUseFailed", [this, otherPlayer], skill);
503
+ const hookTarget = (skill as any)?._skillInstance || skill;
504
+ this["execMethod"]("onUseFailed", [this, otherPlayer], hookTarget);
427
505
  throw SkillLog.chanceToUseFailed(skill);
428
506
  }
429
507
 
@@ -437,7 +515,8 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
437
515
  }
438
516
 
439
517
  // Call onUse hook
440
- this["execMethod"]("onUse", [this, otherPlayer], skill);
518
+ const hookTarget = (skill as any)?._skillInstance || skill;
519
+ this["execMethod"]("onUse", [this, otherPlayer], hookTarget);
441
520
 
442
521
  return skill;
443
522
  }