@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.
- package/dist/Gui/DialogGui.d.ts +1 -0
- package/dist/Gui/GameoverGui.d.ts +23 -0
- package/dist/Gui/Gui.d.ts +6 -0
- package/dist/Gui/MenuGui.d.ts +22 -3
- package/dist/Gui/NotificationGui.d.ts +1 -2
- package/dist/Gui/SaveLoadGui.d.ts +13 -0
- package/dist/Gui/ShopGui.d.ts +24 -3
- package/dist/Gui/TitleGui.d.ts +23 -0
- package/dist/Gui/index.d.ts +9 -1
- package/dist/Player/GuiManager.d.ts +86 -3
- package/dist/Player/Player.d.ts +24 -2
- package/dist/RpgServer.d.ts +7 -0
- package/dist/RpgServerEngine.d.ts +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1621 -336
- package/dist/index.js.map +1 -1
- package/dist/presets/index.d.ts +0 -9
- package/dist/rooms/BaseRoom.d.ts +37 -0
- package/dist/rooms/lobby.d.ts +6 -1
- package/dist/rooms/map.d.ts +22 -1
- package/dist/services/save.d.ts +43 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/localStorage.d.ts +23 -0
- package/package.json +10 -10
- package/src/Gui/DialogGui.ts +12 -2
- package/src/Gui/GameoverGui.ts +39 -0
- package/src/Gui/Gui.ts +23 -1
- package/src/Gui/MenuGui.ts +155 -6
- package/src/Gui/NotificationGui.ts +1 -2
- package/src/Gui/SaveLoadGui.ts +60 -0
- package/src/Gui/ShopGui.ts +145 -16
- package/src/Gui/TitleGui.ts +39 -0
- package/src/Gui/index.ts +13 -2
- package/src/Player/BattleManager.ts +1 -1
- package/src/Player/ClassManager.ts +57 -2
- package/src/Player/GuiManager.ts +125 -14
- package/src/Player/ItemManager.ts +160 -41
- package/src/Player/ParameterManager.ts +1 -1
- package/src/Player/Player.ts +87 -12
- package/src/Player/SkillManager.ts +145 -66
- package/src/Player/StateManager.ts +70 -1
- package/src/Player/VariableManager.ts +10 -7
- package/src/RpgServer.ts +8 -0
- package/src/index.ts +5 -2
- package/src/presets/index.ts +1 -10
- package/src/rooms/BaseRoom.ts +112 -0
- package/src/rooms/lobby.ts +13 -6
- package/src/rooms/map.ts +31 -4
- package/src/services/save.ts +147 -0
- package/src/storage/index.ts +1 -0
- 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 "
|
|
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
|
-
|
|
208
|
+
|
|
209
|
+
private _getItemMap(required: boolean = true) {
|
|
209
210
|
// Use this.map directly to support both RpgMap and LobbyRoom
|
|
210
|
-
|
|
211
|
-
|
|
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 =
|
|
224
|
-
|
|
225
|
-
|
|
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 =
|
|
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 "
|
|
3
|
+
import { MAXHP, MAXSP } from "@rpgjs/common";
|
|
4
4
|
import { sync, type } from "@signe/sync";
|
|
5
5
|
|
|
6
6
|
/**
|
package/src/Player/Player.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
-
|
|
407
|
-
|
|
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
|
|
413
|
-
const data = JSON.parse(snapshot)
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
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):
|
|
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
|
-
|
|
260
|
-
const
|
|
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
|
-
|
|
323
|
-
|
|
396
|
+
const instance = this._createSkillInstance(skillId, skillData, skillInstance);
|
|
397
|
+
|
|
398
|
+
(this as any).skills().push(instance);
|
|
324
399
|
|
|
325
400
|
// Call onLearn hook
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
+
const hookTarget = (skill as any)?._skillInstance || skill;
|
|
519
|
+
this["execMethod"]("onUse", [this, otherPlayer], hookTarget);
|
|
441
520
|
|
|
442
521
|
return skill;
|
|
443
522
|
}
|