@rpgjs/server 5.0.0-beta.3 → 5.0.0-beta.4
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/Player/SkillManager.d.ts +13 -4
- package/dist/RpgServer.d.ts +8 -0
- package/dist/index.js +70 -25
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Gui/MenuGui.ts +30 -20
- package/src/Player/ParameterManager.ts +4 -1
- package/src/Player/Player.ts +2 -0
- package/src/Player/SkillManager.ts +42 -5
- package/src/RpgServer.ts +9 -0
- package/src/rooms/lobby.ts +6 -1
- package/src/rooms/map.ts +17 -0
- package/tests/gui.spec.ts +76 -0
- package/tests/skill.spec.ts +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/server",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.4",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"description": "",
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@canvasengine/tiled": "2.0.0-beta.58",
|
|
25
|
-
"@rpgjs/common": "5.0.0-beta.
|
|
26
|
-
"@rpgjs/physic": "5.0.0-beta.
|
|
27
|
-
"@rpgjs/testing": "5.0.0-beta.
|
|
25
|
+
"@rpgjs/common": "5.0.0-beta.4",
|
|
26
|
+
"@rpgjs/physic": "5.0.0-beta.4",
|
|
27
|
+
"@rpgjs/testing": "5.0.0-beta.4",
|
|
28
28
|
"@rpgjs/database": "^4.3.0",
|
|
29
29
|
"@signe/di": "^2.9.0",
|
|
30
30
|
"@signe/reactive": "^2.9.0",
|
package/src/Gui/MenuGui.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface MenuGuiOptions {
|
|
|
22
22
|
saveAutoSlotLabel?: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function readReactiveValue(value: any, context?: any) {
|
|
26
|
+
return typeof value === 'function' ? value.call(context) : value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readField(source: any, key: string, fallback?: any) {
|
|
30
|
+
const value = source?.[key]
|
|
31
|
+
const resolved = readReactiveValue(value, source)
|
|
32
|
+
return resolved ?? fallback
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export class MenuGui extends Gui {
|
|
26
36
|
private menuOptions: MenuGuiOptions = {}
|
|
27
37
|
|
|
@@ -61,7 +71,7 @@ export class MenuGui extends Gui {
|
|
|
61
71
|
const player = this.player as any
|
|
62
72
|
const databaseById = player.databaseById?.bind(player)
|
|
63
73
|
const equippedIds = new Set(
|
|
64
|
-
(player.equipments?.() || []).map((it) => it
|
|
74
|
+
(player.equipments?.() || []).map((it) => readField(it, 'id', readField(it, 'name')))
|
|
65
75
|
)
|
|
66
76
|
|
|
67
77
|
const buildStats = () => {
|
|
@@ -76,19 +86,19 @@ export class MenuGui extends Gui {
|
|
|
76
86
|
]
|
|
77
87
|
const stats: Record<string, number> = {}
|
|
78
88
|
statKeys.forEach((key) => {
|
|
79
|
-
stats[key] = params[key] ?? 0
|
|
89
|
+
stats[key] = readReactiveValue(params[key]) ?? 0
|
|
80
90
|
})
|
|
81
|
-
stats.pdef = player.pdef ?? params.pdef ?? 0
|
|
82
|
-
stats.sdef = player.sdef ?? params.sdef ?? 0
|
|
83
|
-
stats.atk = player.atk ?? params.atk ?? 0
|
|
91
|
+
stats.pdef = readReactiveValue(player.pdef ?? params.pdef) ?? 0
|
|
92
|
+
stats.sdef = readReactiveValue(player.sdef ?? params.sdef) ?? 0
|
|
93
|
+
stats.atk = readReactiveValue(player.atk ?? params.atk) ?? 0
|
|
84
94
|
return stats
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
const items = (player.items?.() || []).map((item) => {
|
|
88
|
-
const id = item
|
|
98
|
+
const id = readField(item, 'id')
|
|
89
99
|
const data = databaseById ? databaseById(id) : {}
|
|
90
|
-
const type = data
|
|
91
|
-
const consumable = data
|
|
100
|
+
const type = readField(data, '_type', 'item')
|
|
101
|
+
const consumable = readField(data, 'consumable')
|
|
92
102
|
const isConsumable = consumable !== undefined ? consumable : type === 'item'
|
|
93
103
|
const usable = isConsumable === false
|
|
94
104
|
? false
|
|
@@ -97,13 +107,13 @@ export class MenuGui extends Gui {
|
|
|
97
107
|
: true
|
|
98
108
|
return {
|
|
99
109
|
id,
|
|
100
|
-
name: item
|
|
101
|
-
description: item
|
|
102
|
-
quantity: item
|
|
103
|
-
icon: data
|
|
104
|
-
atk: item
|
|
105
|
-
pdef: item
|
|
106
|
-
sdef: item
|
|
110
|
+
name: readField(item, 'name'),
|
|
111
|
+
description: readField(item, 'description'),
|
|
112
|
+
quantity: readField(item, 'quantity'),
|
|
113
|
+
icon: readField(data, 'icon', readField(item, 'icon')),
|
|
114
|
+
atk: readField(item, 'atk'),
|
|
115
|
+
pdef: readField(item, 'pdef'),
|
|
116
|
+
sdef: readField(item, 'sdef'),
|
|
107
117
|
consumable: isConsumable,
|
|
108
118
|
type,
|
|
109
119
|
usable,
|
|
@@ -112,14 +122,14 @@ export class MenuGui extends Gui {
|
|
|
112
122
|
})
|
|
113
123
|
const menuEquips = items.filter((item) => item.type === 'weapon' || item.type === 'armor')
|
|
114
124
|
const skills = (player.skills?.() || []).map((skill) => ({
|
|
115
|
-
id: skill
|
|
116
|
-
name: skill
|
|
117
|
-
description: skill
|
|
118
|
-
spCost: skill
|
|
125
|
+
id: readField(skill, 'id', readField(skill, 'name')),
|
|
126
|
+
name: readField(skill, 'name', readField(skill, 'id', 'Skill')),
|
|
127
|
+
description: readField(skill, 'description', ''),
|
|
128
|
+
spCost: readField(skill, 'spCost', 0)
|
|
119
129
|
}))
|
|
120
130
|
const saveLoad = this.buildSaveLoad(options)
|
|
121
131
|
|
|
122
|
-
return { menus, items, equips: menuEquips, skills, saveLoad, playerStats: buildStats(), expForNextlevel: player.expForNextlevel }
|
|
132
|
+
return { menus, items, equips: menuEquips, skills, saveLoad, playerStats: buildStats(), expForNextlevel: readReactiveValue(player.expForNextlevel) }
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
private refreshMenu(clientActionId?: string) {
|
|
@@ -753,7 +753,10 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
753
753
|
for (let i = this._level() ; i <= val; i++) {
|
|
754
754
|
for (let skill of currentClass.skillsToLearn as any[]) {
|
|
755
755
|
if (skill.level == i) {
|
|
756
|
-
this['learnSkill'](skill.skill
|
|
756
|
+
this['learnSkill'](skill.skill, {
|
|
757
|
+
source: skill.source ?? 'level',
|
|
758
|
+
level: i
|
|
759
|
+
})
|
|
757
760
|
}
|
|
758
761
|
}
|
|
759
762
|
}
|
package/src/Player/Player.ts
CHANGED
|
@@ -12,7 +12,20 @@ import { Effect } from "./EffectManager";
|
|
|
12
12
|
/**
|
|
13
13
|
* Type for skill class constructor
|
|
14
14
|
*/
|
|
15
|
-
type SkillClass = { new (...args: any[]): any };
|
|
15
|
+
export type SkillClass = { new (...args: any[]): any };
|
|
16
|
+
|
|
17
|
+
export type SkillChangeAction = "learn" | "forget";
|
|
18
|
+
|
|
19
|
+
export interface SkillChangeOptions {
|
|
20
|
+
source?: "manual" | "level" | "class" | "studio" | string;
|
|
21
|
+
level?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SkillChangePayload extends SkillChangeOptions {
|
|
25
|
+
action: SkillChangeAction;
|
|
26
|
+
skill: SkillClass | SkillObject | string;
|
|
27
|
+
skillId: string;
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
/**
|
|
18
31
|
* Interface defining the hooks that can be implemented on skill classes or objects
|
|
@@ -430,7 +443,10 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
|
|
|
430
443
|
* });
|
|
431
444
|
* ```
|
|
432
445
|
*/
|
|
433
|
-
learnSkill(
|
|
446
|
+
learnSkill(
|
|
447
|
+
skillInput: SkillClass | SkillObject | string,
|
|
448
|
+
options: SkillChangeOptions = {},
|
|
449
|
+
): any {
|
|
434
450
|
const map = this._getSkillMap();
|
|
435
451
|
const { skillId, skillData, skillInstance } = this._resolveSkillInput(skillInput, map);
|
|
436
452
|
|
|
@@ -446,6 +462,15 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
|
|
|
446
462
|
// Call onLearn hook
|
|
447
463
|
const hookTarget = (instance as any)._skillInstance || instance;
|
|
448
464
|
this["execMethod"]("onLearn", [this], hookTarget);
|
|
465
|
+
this["execMethod"]("onSkillChange", [
|
|
466
|
+
{
|
|
467
|
+
action: "learn",
|
|
468
|
+
skill: skillData,
|
|
469
|
+
skillId,
|
|
470
|
+
source: options.source ?? "manual",
|
|
471
|
+
level: options.level,
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
449
474
|
|
|
450
475
|
return skillData;
|
|
451
476
|
}
|
|
@@ -466,7 +491,10 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
|
|
|
466
491
|
* player.forgetSkill(FireSkill);
|
|
467
492
|
* ```
|
|
468
493
|
*/
|
|
469
|
-
forgetSkill(
|
|
494
|
+
forgetSkill(
|
|
495
|
+
skillInput: SkillClass | SkillObject | string,
|
|
496
|
+
options: SkillChangeOptions = {},
|
|
497
|
+
): any {
|
|
470
498
|
const index = this._getSkillIndex(skillInput);
|
|
471
499
|
|
|
472
500
|
if (index === -1) {
|
|
@@ -494,6 +522,15 @@ export function WithSkillManager<TBase extends PlayerCtor>(Base: TBase): TBase {
|
|
|
494
522
|
(skillEntry as any)?._skillData ||
|
|
495
523
|
skillData;
|
|
496
524
|
this["execMethod"]("onForget", [this], hookTarget);
|
|
525
|
+
this["execMethod"]("onSkillChange", [
|
|
526
|
+
{
|
|
527
|
+
action: "forget",
|
|
528
|
+
skill: skillData,
|
|
529
|
+
skillId: skillData?.id ?? String(skillInput),
|
|
530
|
+
source: options.source ?? "manual",
|
|
531
|
+
level: options.level,
|
|
532
|
+
},
|
|
533
|
+
]);
|
|
497
534
|
|
|
498
535
|
return skillData;
|
|
499
536
|
}
|
|
@@ -607,7 +644,7 @@ export interface ISkillManager {
|
|
|
607
644
|
* @returns The learned skill data
|
|
608
645
|
* @throws SkillLog.alreadyLearned if the player already knows the skill
|
|
609
646
|
*/
|
|
610
|
-
learnSkill(skillInput: SkillClass | SkillObject | string): any;
|
|
647
|
+
learnSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): any;
|
|
611
648
|
|
|
612
649
|
/**
|
|
613
650
|
* Forget a skill
|
|
@@ -616,7 +653,7 @@ export interface ISkillManager {
|
|
|
616
653
|
* @returns The forgotten skill data
|
|
617
654
|
* @throws SkillLog.notLearned if trying to forget a skill not learned
|
|
618
655
|
*/
|
|
619
|
-
forgetSkill(skillInput: SkillClass | SkillObject | string): any;
|
|
656
|
+
forgetSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): any;
|
|
620
657
|
|
|
621
658
|
/**
|
|
622
659
|
* Use a skill
|
package/src/RpgServer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type RpgMap } from "./rooms/map"
|
|
|
4
4
|
import { RpgServerEngine } from "./RpgServerEngine"
|
|
5
5
|
import { WorldMapConfig, RpgShape, type MapPhysicsInitContext, type MapPhysicsEntityContext } from "@rpgjs/common"
|
|
6
6
|
import { RpgEvent } from "./Player/Player"
|
|
7
|
+
import type { SkillChangePayload } from "./Player/SkillManager"
|
|
7
8
|
|
|
8
9
|
type RpgClassMap<T> = new () => T
|
|
9
10
|
type RpgClassEvent<T> = RpgEvent
|
|
@@ -210,6 +211,14 @@ export interface RpgPlayerHooks {
|
|
|
210
211
|
*/
|
|
211
212
|
onLevelUp?: (player: RpgPlayer, nbLevel: number) => any
|
|
212
213
|
|
|
214
|
+
/**
|
|
215
|
+
* When a player learns or forgets a skill
|
|
216
|
+
*
|
|
217
|
+
* @prop { (player: RpgPlayer, payload: SkillChangePayload) => any } [onSkillChange]
|
|
218
|
+
* @memberof RpgPlayerHooks
|
|
219
|
+
*/
|
|
220
|
+
onSkillChange?: (player: RpgPlayer, payload: SkillChangePayload) => any
|
|
221
|
+
|
|
213
222
|
/**
|
|
214
223
|
* When the player's HP drops to 0
|
|
215
224
|
*
|
package/src/rooms/lobby.ts
CHANGED
|
@@ -36,7 +36,12 @@ export class LobbyRoom extends BaseRoom {
|
|
|
36
36
|
const id = value.data.id
|
|
37
37
|
if (id === 'start') {
|
|
38
38
|
player.initializeDefaultStats();
|
|
39
|
-
|
|
39
|
+
try {
|
|
40
|
+
await lastValueFrom(this.hooks.callHooks("server-player-onStart", player));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("[RPGJS] Error during player onStart hooks:", error);
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
}
|
package/src/rooms/map.ts
CHANGED
|
@@ -1091,6 +1091,13 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1091
1091
|
*/
|
|
1092
1092
|
@Action('move')
|
|
1093
1093
|
async onInput(player: RpgPlayer, input: any) {
|
|
1094
|
+
if (typeof player.canMove === "function" && !player.canMove()) {
|
|
1095
|
+
player.pendingInputs = [];
|
|
1096
|
+
player.lastProcessedInputTs = 0;
|
|
1097
|
+
(this as any).stopMovement(player);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1094
1101
|
const lastAckedFrame = player._lastFramePositions?.frame ?? 0;
|
|
1095
1102
|
const now = Date.now();
|
|
1096
1103
|
const candidates: Array<{
|
|
@@ -1525,6 +1532,16 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
|
1525
1532
|
}
|
|
1526
1533
|
}
|
|
1527
1534
|
|
|
1535
|
+
if (typeof player.canMove === "function" && !player.canMove()) {
|
|
1536
|
+
player.pendingInputs = [];
|
|
1537
|
+
player.lastProcessedInputTs = 0;
|
|
1538
|
+
(this as any).stopMovement(player);
|
|
1539
|
+
return {
|
|
1540
|
+
player,
|
|
1541
|
+
inputs: []
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1528
1545
|
const processedInputs: string[] = [];
|
|
1529
1546
|
const defaultControls: Required<Controls> = {
|
|
1530
1547
|
maxTimeDelta: 1000, // 1 second max between inputs
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { MenuGui, signal } from "../src";
|
|
3
|
+
|
|
4
|
+
describe("GUI", () => {
|
|
5
|
+
test("main menu sends cloneable data when inventory data contains signals", () => {
|
|
6
|
+
const inventoryItem = {
|
|
7
|
+
id: signal("sword"),
|
|
8
|
+
name: signal("Bronze Sword"),
|
|
9
|
+
description: signal("A starter weapon"),
|
|
10
|
+
quantity: signal(1),
|
|
11
|
+
atk: signal(5),
|
|
12
|
+
pdef: signal(0),
|
|
13
|
+
sdef: signal(0),
|
|
14
|
+
icon: signal("inventory-icon"),
|
|
15
|
+
};
|
|
16
|
+
const skill = {
|
|
17
|
+
id: signal("fire"),
|
|
18
|
+
name: signal("Fire"),
|
|
19
|
+
description: signal("Small fire spell"),
|
|
20
|
+
spCost: signal(3),
|
|
21
|
+
};
|
|
22
|
+
const sent: any[] = [];
|
|
23
|
+
const player: any = {
|
|
24
|
+
canMove: signal(true),
|
|
25
|
+
items: signal([inventoryItem]),
|
|
26
|
+
equipments: signal([inventoryItem]),
|
|
27
|
+
skills: signal([skill]),
|
|
28
|
+
param: { str: 4, dex: 3, int: 2, agi: 1, maxHp: 20, maxSp: 10 },
|
|
29
|
+
pdef: 1,
|
|
30
|
+
sdef: 2,
|
|
31
|
+
atk: 5,
|
|
32
|
+
expForNextlevel: signal(150),
|
|
33
|
+
databaseById() {
|
|
34
|
+
return {
|
|
35
|
+
_type: signal("weapon"),
|
|
36
|
+
icon: signal("db-icon"),
|
|
37
|
+
consumable: signal(false),
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
emit(type: string, value: any) {
|
|
41
|
+
sent.push(structuredClone({ type, value }));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const gui = new MenuGui(player);
|
|
46
|
+
const pending = gui.open();
|
|
47
|
+
|
|
48
|
+
expect(sent[0]).toMatchObject({
|
|
49
|
+
type: "gui.open",
|
|
50
|
+
value: {
|
|
51
|
+
guiId: "rpg-main-menu",
|
|
52
|
+
data: {
|
|
53
|
+
expForNextlevel: 150,
|
|
54
|
+
items: [
|
|
55
|
+
{
|
|
56
|
+
id: "sword",
|
|
57
|
+
icon: "db-icon",
|
|
58
|
+
type: "weapon",
|
|
59
|
+
equipped: true,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
skills: [
|
|
63
|
+
{
|
|
64
|
+
id: "fire",
|
|
65
|
+
name: "Fire",
|
|
66
|
+
spCost: 3,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
gui.close();
|
|
74
|
+
return pending;
|
|
75
|
+
});
|
|
76
|
+
});
|
package/tests/skill.spec.ts
CHANGED
|
@@ -92,6 +92,7 @@ const FreeSkill = {
|
|
|
92
92
|
|
|
93
93
|
let player: RpgPlayer;
|
|
94
94
|
let fixture: TestingFixture;
|
|
95
|
+
const onSkillChangeSpy = vi.fn();
|
|
95
96
|
|
|
96
97
|
// Define server module with skills in database
|
|
97
98
|
const serverModule = defineModule({
|
|
@@ -113,6 +114,7 @@ const serverModule = defineModule({
|
|
|
113
114
|
async onConnected(player) {
|
|
114
115
|
await player.changeMap("test-map", { x: 100, y: 100 });
|
|
115
116
|
},
|
|
117
|
+
onSkillChange: onSkillChangeSpy,
|
|
116
118
|
},
|
|
117
119
|
});
|
|
118
120
|
|
|
@@ -122,6 +124,7 @@ const clientModule = defineModule({
|
|
|
122
124
|
});
|
|
123
125
|
|
|
124
126
|
beforeEach(async () => {
|
|
127
|
+
onSkillChangeSpy.mockClear();
|
|
125
128
|
const myModule = createModule("TestModule", [
|
|
126
129
|
{
|
|
127
130
|
server: serverModule,
|
|
@@ -523,6 +526,51 @@ describe("Skill Management - Hooks", () => {
|
|
|
523
526
|
expect(onForgetSpy).toHaveBeenCalledWith(player);
|
|
524
527
|
});
|
|
525
528
|
|
|
529
|
+
test("should call player onSkillChange hook when learning skill", () => {
|
|
530
|
+
const skill = player.learnSkill("fire");
|
|
531
|
+
|
|
532
|
+
expect(skill).toBe(FireSkill);
|
|
533
|
+
expect(onSkillChangeSpy).toHaveBeenCalledWith(
|
|
534
|
+
player,
|
|
535
|
+
expect.objectContaining({
|
|
536
|
+
action: "learn",
|
|
537
|
+
skill: FireSkill,
|
|
538
|
+
skillId: "fire",
|
|
539
|
+
source: "manual",
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("should call player onSkillChange hook when forgetting skill", () => {
|
|
545
|
+
player.learnSkill("fire");
|
|
546
|
+
onSkillChangeSpy.mockClear();
|
|
547
|
+
|
|
548
|
+
player.forgetSkill("fire");
|
|
549
|
+
|
|
550
|
+
expect(onSkillChangeSpy).toHaveBeenCalledWith(
|
|
551
|
+
player,
|
|
552
|
+
expect.objectContaining({
|
|
553
|
+
action: "forget",
|
|
554
|
+
skillId: "fire",
|
|
555
|
+
source: "manual",
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("should pass source and level to onSkillChange hook", () => {
|
|
561
|
+
player.learnSkill("fire", { source: "level", level: 3 });
|
|
562
|
+
|
|
563
|
+
expect(onSkillChangeSpy).toHaveBeenCalledWith(
|
|
564
|
+
player,
|
|
565
|
+
expect.objectContaining({
|
|
566
|
+
action: "learn",
|
|
567
|
+
skillId: "fire",
|
|
568
|
+
source: "level",
|
|
569
|
+
level: 3,
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
526
574
|
test("should call onUse hook when using skill successfully", () => {
|
|
527
575
|
const onUseSpy = vi.fn();
|
|
528
576
|
const customSkill: SkillObject = {
|