@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.
- package/dist/Player/MoveManager.d.ts +115 -50
- package/dist/Player/Player.d.ts +43 -19
- package/dist/Player/SkillManager.d.ts +157 -22
- package/dist/index.js +1007 -197
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/Player/MoveManager.ts +659 -213
- package/src/Player/Player.ts +89 -20
- package/src/Player/SkillManager.ts +401 -73
- package/src/rooms/map.ts +18 -0
- package/tests/battle.spec.ts +375 -0
- package/tests/class.spec.ts +274 -0
- package/tests/effect.spec.ts +219 -0
- package/tests/element.spec.ts +221 -0
- package/tests/gold.spec.ts +99 -0
- package/tests/skill.spec.ts +658 -0
- package/tests/state.spec.ts +467 -0
- package/tests/variable.spec.ts +185 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { beforeEach, test, expect, afterEach, describe, vi } from "vitest";
|
|
2
|
+
import { testing, TestingFixture } from "@rpgjs/testing";
|
|
3
|
+
import { defineModule, createModule } from "@rpgjs/common";
|
|
4
|
+
import { RpgPlayer, MAXSP, ATK, PDEF } from "../src";
|
|
5
|
+
import { Effect } from "../src/Player/EffectManager";
|
|
6
|
+
import type { SkillObject } from "../src/Player/SkillManager";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test skill object for basic skill tests
|
|
10
|
+
*/
|
|
11
|
+
const FireSkill = {
|
|
12
|
+
id: "fire",
|
|
13
|
+
name: "Fire",
|
|
14
|
+
description: "A basic fire spell",
|
|
15
|
+
spCost: 10,
|
|
16
|
+
hitRate: 1,
|
|
17
|
+
power: 50,
|
|
18
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
19
|
+
_type: "skill" as const,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Test skill with high SP cost
|
|
24
|
+
*/
|
|
25
|
+
const UltimateSkill = {
|
|
26
|
+
id: "ultimate",
|
|
27
|
+
name: "Ultimate Strike",
|
|
28
|
+
description: "A powerful ultimate skill",
|
|
29
|
+
spCost: 100,
|
|
30
|
+
hitRate: 1,
|
|
31
|
+
power: 200,
|
|
32
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
33
|
+
_type: "skill" as const,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Test skill with low hit rate (50% chance)
|
|
38
|
+
*/
|
|
39
|
+
const LowHitRateSkill = {
|
|
40
|
+
id: "low-hit-rate",
|
|
41
|
+
name: "Risky Attack",
|
|
42
|
+
description: "A skill with low success rate",
|
|
43
|
+
spCost: 5,
|
|
44
|
+
hitRate: 0.5,
|
|
45
|
+
power: 100,
|
|
46
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
47
|
+
_type: "skill" as const,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Test skill with 0% hit rate (always fails)
|
|
52
|
+
*/
|
|
53
|
+
const AlwaysFailSkill = {
|
|
54
|
+
id: "always-fail",
|
|
55
|
+
name: "Always Fail",
|
|
56
|
+
description: "A skill that always fails",
|
|
57
|
+
spCost: 5,
|
|
58
|
+
hitRate: 0,
|
|
59
|
+
power: 50,
|
|
60
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
61
|
+
_type: "skill" as const,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Test healing skill (no damage, heals target)
|
|
66
|
+
*/
|
|
67
|
+
const HealSkill = {
|
|
68
|
+
id: "heal",
|
|
69
|
+
name: "Heal",
|
|
70
|
+
description: "Restores HP to target",
|
|
71
|
+
spCost: 15,
|
|
72
|
+
hitRate: 1,
|
|
73
|
+
hpValue: 50,
|
|
74
|
+
power: 0,
|
|
75
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
76
|
+
_type: "skill" as const,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Test free skill (no SP cost)
|
|
81
|
+
*/
|
|
82
|
+
const FreeSkill = {
|
|
83
|
+
id: "free-skill",
|
|
84
|
+
name: "Free Skill",
|
|
85
|
+
description: "A skill with no cost",
|
|
86
|
+
spCost: 0,
|
|
87
|
+
hitRate: 1,
|
|
88
|
+
power: 10,
|
|
89
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
90
|
+
_type: "skill" as const,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let player: RpgPlayer;
|
|
94
|
+
let fixture: TestingFixture;
|
|
95
|
+
|
|
96
|
+
// Define server module with skills in database
|
|
97
|
+
const serverModule = defineModule({
|
|
98
|
+
maps: [
|
|
99
|
+
{
|
|
100
|
+
id: "test-map",
|
|
101
|
+
file: "",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
database: {
|
|
105
|
+
fire: FireSkill,
|
|
106
|
+
ultimate: UltimateSkill,
|
|
107
|
+
"low-hit-rate": LowHitRateSkill,
|
|
108
|
+
"always-fail": AlwaysFailSkill,
|
|
109
|
+
heal: HealSkill,
|
|
110
|
+
"free-skill": FreeSkill,
|
|
111
|
+
},
|
|
112
|
+
player: {
|
|
113
|
+
async onConnected(player) {
|
|
114
|
+
await player.changeMap("test-map", { x: 100, y: 100 });
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Define client module
|
|
120
|
+
const clientModule = defineModule({
|
|
121
|
+
// Client-side logic
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
beforeEach(async () => {
|
|
125
|
+
const myModule = createModule("TestModule", [
|
|
126
|
+
{
|
|
127
|
+
server: serverModule,
|
|
128
|
+
client: clientModule,
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
fixture = await testing(myModule);
|
|
133
|
+
const clientTesting = await fixture.createClient();
|
|
134
|
+
player = await clientTesting.waitForMapChange("test-map");
|
|
135
|
+
|
|
136
|
+
// Initialize player SP for skill tests
|
|
137
|
+
player.sp = 100;
|
|
138
|
+
player.param[MAXSP] = 100;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await fixture.clear();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("Skill Management - Learning Skills", () => {
|
|
146
|
+
test("should learn a skill using string ID", () => {
|
|
147
|
+
const skill = player.learnSkill("fire");
|
|
148
|
+
expect(skill).toBeDefined();
|
|
149
|
+
expect(skill.id).toBe("fire");
|
|
150
|
+
expect(skill.name).toBe("Fire");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("should learn a skill using object", () => {
|
|
154
|
+
const customSkill: SkillObject = {
|
|
155
|
+
id: "custom-skill",
|
|
156
|
+
name: "Custom Skill",
|
|
157
|
+
spCost: 20,
|
|
158
|
+
hitRate: 1,
|
|
159
|
+
power: 30,
|
|
160
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
161
|
+
_type: "skill",
|
|
162
|
+
};
|
|
163
|
+
const skill = player.learnSkill(customSkill);
|
|
164
|
+
expect(skill).toBeDefined();
|
|
165
|
+
expect(skill.id).toBe("custom-skill");
|
|
166
|
+
expect(skill.name).toBe("Custom Skill");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should auto-generate ID for skill object without ID", () => {
|
|
170
|
+
const customSkill: SkillObject = {
|
|
171
|
+
name: "Auto ID Skill",
|
|
172
|
+
spCost: 5,
|
|
173
|
+
hitRate: 1,
|
|
174
|
+
};
|
|
175
|
+
const skill = player.learnSkill(customSkill);
|
|
176
|
+
expect(skill).toBeDefined();
|
|
177
|
+
expect(skill.id).toMatch(/^skill-\d+$/);
|
|
178
|
+
expect(skill.name).toBe("Auto ID Skill");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("should throw error when learning already learned skill", () => {
|
|
182
|
+
player.learnSkill("fire");
|
|
183
|
+
expect(() => {
|
|
184
|
+
player.learnSkill("fire");
|
|
185
|
+
}).toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("should throw error when learning already learned skill using object", () => {
|
|
189
|
+
player.learnSkill("fire");
|
|
190
|
+
const duplicateSkill: SkillObject = {
|
|
191
|
+
id: "fire",
|
|
192
|
+
name: "Duplicate Fire",
|
|
193
|
+
};
|
|
194
|
+
expect(() => {
|
|
195
|
+
player.learnSkill(duplicateSkill);
|
|
196
|
+
}).toThrow();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("should learn multiple different skills", () => {
|
|
200
|
+
player.learnSkill("fire");
|
|
201
|
+
player.learnSkill("heal");
|
|
202
|
+
player.learnSkill("free-skill");
|
|
203
|
+
|
|
204
|
+
expect(player.getSkill("fire")).toBeDefined();
|
|
205
|
+
expect(player.getSkill("heal")).toBeDefined();
|
|
206
|
+
expect(player.getSkill("free-skill")).toBeDefined();
|
|
207
|
+
expect(player.skills().length).toBe(3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("should learn skill with object and use it", () => {
|
|
211
|
+
const customSkill: SkillObject = {
|
|
212
|
+
id: "direct-skill",
|
|
213
|
+
name: "Direct Skill",
|
|
214
|
+
spCost: 5,
|
|
215
|
+
hitRate: 1,
|
|
216
|
+
power: 0,
|
|
217
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
218
|
+
};
|
|
219
|
+
player.learnSkill(customSkill);
|
|
220
|
+
const initialSp = player.sp;
|
|
221
|
+
const skill = player.useSkill("direct-skill");
|
|
222
|
+
expect(skill).toBeDefined();
|
|
223
|
+
expect(player.sp).toBe(initialSp - 5);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("Skill Management - Getting Skills", () => {
|
|
228
|
+
test("should get learned skill by string ID", () => {
|
|
229
|
+
player.learnSkill("fire");
|
|
230
|
+
const skill = player.getSkill("fire");
|
|
231
|
+
expect(skill).toBeDefined();
|
|
232
|
+
expect(skill.id).toBe("fire");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("should get learned skill by object", () => {
|
|
236
|
+
const customSkill: SkillObject = {
|
|
237
|
+
id: "get-test-skill",
|
|
238
|
+
name: "Get Test Skill",
|
|
239
|
+
spCost: 10,
|
|
240
|
+
};
|
|
241
|
+
player.learnSkill(customSkill);
|
|
242
|
+
const skill = player.getSkill({ id: "get-test-skill" });
|
|
243
|
+
expect(skill).toBeDefined();
|
|
244
|
+
expect(skill.id).toBe("get-test-skill");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("should return null for non-learned skill", () => {
|
|
248
|
+
const skill = player.getSkill("fire");
|
|
249
|
+
expect(skill).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("Skill Management - Forgetting Skills", () => {
|
|
254
|
+
test("should forget a learned skill using string ID", () => {
|
|
255
|
+
player.learnSkill("fire");
|
|
256
|
+
const forgottenSkill = player.forgetSkill("fire");
|
|
257
|
+
expect(forgottenSkill).toBeDefined();
|
|
258
|
+
expect(player.getSkill("fire")).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("should forget a learned skill using object", () => {
|
|
262
|
+
const customSkill: SkillObject = {
|
|
263
|
+
id: "forget-test-skill",
|
|
264
|
+
name: "Forget Test Skill",
|
|
265
|
+
spCost: 10,
|
|
266
|
+
};
|
|
267
|
+
player.learnSkill(customSkill);
|
|
268
|
+
const forgottenSkill = player.forgetSkill({ id: "forget-test-skill" });
|
|
269
|
+
expect(forgottenSkill).toBeDefined();
|
|
270
|
+
expect(player.getSkill("forget-test-skill")).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("should throw error when forgetting non-learned skill", () => {
|
|
274
|
+
expect(() => {
|
|
275
|
+
player.forgetSkill("fire");
|
|
276
|
+
}).toThrow();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("should be able to relearn forgotten skill", () => {
|
|
280
|
+
player.learnSkill("fire");
|
|
281
|
+
player.forgetSkill("fire");
|
|
282
|
+
const skill = player.learnSkill("fire");
|
|
283
|
+
expect(skill).toBeDefined();
|
|
284
|
+
expect(player.getSkill("fire")).toBeDefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("should maintain skill list integrity after operations", () => {
|
|
288
|
+
player.learnSkill("fire");
|
|
289
|
+
player.learnSkill("heal");
|
|
290
|
+
player.learnSkill("free-skill");
|
|
291
|
+
|
|
292
|
+
expect(player.skills().length).toBe(3);
|
|
293
|
+
|
|
294
|
+
player.forgetSkill("heal");
|
|
295
|
+
expect(player.skills().length).toBe(2);
|
|
296
|
+
expect(player.getSkill("fire")).toBeDefined();
|
|
297
|
+
expect(player.getSkill("heal")).toBeNull();
|
|
298
|
+
expect(player.getSkill("free-skill")).toBeDefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("Skill Management - Using Skills", () => {
|
|
303
|
+
test("should use skill and consume SP", () => {
|
|
304
|
+
player.learnSkill("fire");
|
|
305
|
+
const initialSp = player.sp;
|
|
306
|
+
const skill = player.useSkill("fire");
|
|
307
|
+
expect(skill).toBeDefined();
|
|
308
|
+
expect(player.sp).toBe(initialSp - FireSkill.spCost);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("should use free skill without consuming SP", () => {
|
|
312
|
+
player.learnSkill("free-skill");
|
|
313
|
+
const initialSp = player.sp;
|
|
314
|
+
player.useSkill("free-skill");
|
|
315
|
+
expect(player.sp).toBe(initialSp);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("should throw error when using non-learned skill", () => {
|
|
319
|
+
expect(() => {
|
|
320
|
+
player.useSkill("fire");
|
|
321
|
+
}).toThrow();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("should throw error when not enough SP", () => {
|
|
325
|
+
player.sp = 5; // Not enough for FireSkill (costs 10)
|
|
326
|
+
player.learnSkill("fire");
|
|
327
|
+
expect(() => {
|
|
328
|
+
player.useSkill("fire");
|
|
329
|
+
}).toThrow();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("should throw error when SP is exactly 0", () => {
|
|
333
|
+
player.sp = 0;
|
|
334
|
+
player.learnSkill("fire");
|
|
335
|
+
expect(() => {
|
|
336
|
+
player.useSkill("fire");
|
|
337
|
+
}).toThrow();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("should use skill with exact SP amount", () => {
|
|
341
|
+
player.sp = FireSkill.spCost; // Exactly enough
|
|
342
|
+
player.learnSkill("fire");
|
|
343
|
+
const skill = player.useSkill("fire");
|
|
344
|
+
expect(skill).toBeDefined();
|
|
345
|
+
expect(player.sp).toBe(0);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("Skill Management - Hit Rate", () => {
|
|
350
|
+
test("should use skill successfully when hitRate passes", () => {
|
|
351
|
+
const originalRandom = Math.random;
|
|
352
|
+
Math.random = vi.fn(() => 0.3); // 0.3 < 0.5 (hitRate)
|
|
353
|
+
|
|
354
|
+
player.learnSkill("low-hit-rate");
|
|
355
|
+
const skill = player.useSkill("low-hit-rate");
|
|
356
|
+
expect(skill).toBeDefined();
|
|
357
|
+
|
|
358
|
+
Math.random = originalRandom;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("should fail when hitRate check fails", () => {
|
|
362
|
+
const originalRandom = Math.random;
|
|
363
|
+
Math.random = vi.fn(() => 0.9); // 0.9 > 0.5 (hitRate)
|
|
364
|
+
|
|
365
|
+
player.learnSkill("low-hit-rate");
|
|
366
|
+
expect(() => {
|
|
367
|
+
player.useSkill("low-hit-rate");
|
|
368
|
+
}).toThrow();
|
|
369
|
+
|
|
370
|
+
Math.random = originalRandom;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("should always fail with 0 hitRate", () => {
|
|
374
|
+
const originalRandom = Math.random;
|
|
375
|
+
Math.random = vi.fn(() => 0.001); // Even very small number > 0
|
|
376
|
+
|
|
377
|
+
player.learnSkill("always-fail");
|
|
378
|
+
expect(() => {
|
|
379
|
+
player.useSkill("always-fail");
|
|
380
|
+
}).toThrow();
|
|
381
|
+
|
|
382
|
+
Math.random = originalRandom;
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("should still consume SP even when hitRate fails", () => {
|
|
386
|
+
const originalRandom = Math.random;
|
|
387
|
+
Math.random = vi.fn(() => 0.9); // Will fail
|
|
388
|
+
|
|
389
|
+
player.learnSkill("low-hit-rate");
|
|
390
|
+
const initialSp = player.sp;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
player.useSkill("low-hit-rate");
|
|
394
|
+
} catch {
|
|
395
|
+
// Expected to throw
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// SP should be consumed even on failure
|
|
399
|
+
expect(player.sp).toBe(initialSp - LowHitRateSkill.spCost);
|
|
400
|
+
|
|
401
|
+
Math.random = originalRandom;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("should always succeed with hitRate of 1", () => {
|
|
405
|
+
const originalRandom = Math.random;
|
|
406
|
+
Math.random = vi.fn(() => 0.99);
|
|
407
|
+
|
|
408
|
+
player.learnSkill("fire"); // hitRate: 1
|
|
409
|
+
const skill = player.useSkill("fire");
|
|
410
|
+
expect(skill).toBeDefined();
|
|
411
|
+
|
|
412
|
+
Math.random = originalRandom;
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("Skill Management - Effects and Restrictions", () => {
|
|
417
|
+
test("should throw error when player has CAN_NOT_SKILL effect", () => {
|
|
418
|
+
player.learnSkill("fire");
|
|
419
|
+
player.effects = [Effect.CAN_NOT_SKILL];
|
|
420
|
+
expect(() => {
|
|
421
|
+
player.useSkill("fire");
|
|
422
|
+
}).toThrow();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("should use half SP cost with HALF_SP_COST effect", () => {
|
|
426
|
+
player.learnSkill("fire");
|
|
427
|
+
player.effects = [Effect.HALF_SP_COST];
|
|
428
|
+
const initialSp = player.sp;
|
|
429
|
+
|
|
430
|
+
player.useSkill("fire");
|
|
431
|
+
expect(player.sp).toBe(initialSp - FireSkill.spCost / 2);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("should use half SP cost rounded correctly", () => {
|
|
435
|
+
player.learnSkill("low-hit-rate"); // spCost: 5
|
|
436
|
+
|
|
437
|
+
// Mock random to pass
|
|
438
|
+
const originalRandom = Math.random;
|
|
439
|
+
Math.random = vi.fn(() => 0.1);
|
|
440
|
+
|
|
441
|
+
player.effects = [Effect.HALF_SP_COST];
|
|
442
|
+
const initialSp = player.sp;
|
|
443
|
+
|
|
444
|
+
player.useSkill("low-hit-rate");
|
|
445
|
+
expect(player.sp).toBe(initialSp - LowHitRateSkill.spCost / 2);
|
|
446
|
+
|
|
447
|
+
Math.random = originalRandom;
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("Skill Management - Using Skills on Targets", () => {
|
|
452
|
+
let targetPlayer: RpgPlayer;
|
|
453
|
+
|
|
454
|
+
beforeEach(async () => {
|
|
455
|
+
const clientTesting2 = await fixture.createClient();
|
|
456
|
+
targetPlayer = await clientTesting2.waitForMapChange("test-map");
|
|
457
|
+
targetPlayer.sp = 100;
|
|
458
|
+
targetPlayer.param[MAXSP] = 100;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("should use skill on single target", () => {
|
|
462
|
+
const originalRandom = Math.random;
|
|
463
|
+
Math.random = vi.fn(() => 0.1);
|
|
464
|
+
|
|
465
|
+
player.learnSkill("fire");
|
|
466
|
+
const skill = player.useSkill("fire", targetPlayer);
|
|
467
|
+
expect(skill).toBeDefined();
|
|
468
|
+
|
|
469
|
+
Math.random = originalRandom;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("should use skill on multiple targets", () => {
|
|
473
|
+
const originalRandom = Math.random;
|
|
474
|
+
Math.random = vi.fn(() => 0.1);
|
|
475
|
+
|
|
476
|
+
player.learnSkill("fire");
|
|
477
|
+
const skill = player.useSkill("fire", [targetPlayer]);
|
|
478
|
+
expect(skill).toBeDefined();
|
|
479
|
+
|
|
480
|
+
Math.random = originalRandom;
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("should use healing skill on target", () => {
|
|
484
|
+
player.learnSkill("heal");
|
|
485
|
+
const skill = player.useSkill("heal", targetPlayer);
|
|
486
|
+
expect(skill).toBeDefined();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("Skill Management - Hooks", () => {
|
|
491
|
+
test("should call onLearn hook when learning skill", () => {
|
|
492
|
+
const onLearnSpy = vi.fn();
|
|
493
|
+
const customSkill: SkillObject = {
|
|
494
|
+
id: "learn-hook-skill",
|
|
495
|
+
name: "Learn Hook Skill",
|
|
496
|
+
spCost: 10,
|
|
497
|
+
hitRate: 1,
|
|
498
|
+
power: 0,
|
|
499
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
500
|
+
_type: "skill",
|
|
501
|
+
onLearn: onLearnSpy,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
player.learnSkill(customSkill);
|
|
505
|
+
expect(onLearnSpy).toHaveBeenCalledWith(player);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("should call onForget hook when forgetting skill", () => {
|
|
509
|
+
const onForgetSpy = vi.fn();
|
|
510
|
+
const customSkill: SkillObject = {
|
|
511
|
+
id: "forget-hook-skill",
|
|
512
|
+
name: "Forget Hook Skill",
|
|
513
|
+
spCost: 10,
|
|
514
|
+
hitRate: 1,
|
|
515
|
+
power: 0,
|
|
516
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
517
|
+
_type: "skill",
|
|
518
|
+
onForget: onForgetSpy,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
player.learnSkill(customSkill);
|
|
522
|
+
player.forgetSkill("forget-hook-skill");
|
|
523
|
+
expect(onForgetSpy).toHaveBeenCalledWith(player);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("should call onUse hook when using skill successfully", () => {
|
|
527
|
+
const onUseSpy = vi.fn();
|
|
528
|
+
const customSkill: SkillObject = {
|
|
529
|
+
id: "use-hook-skill",
|
|
530
|
+
name: "Use Hook Skill",
|
|
531
|
+
spCost: 10,
|
|
532
|
+
hitRate: 1,
|
|
533
|
+
power: 0,
|
|
534
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
535
|
+
_type: "skill",
|
|
536
|
+
onUse: onUseSpy,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
player.learnSkill(customSkill);
|
|
540
|
+
player.useSkill("use-hook-skill");
|
|
541
|
+
expect(onUseSpy).toHaveBeenCalledWith(player, undefined);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("should call onUse hook with target when provided", async () => {
|
|
545
|
+
const onUseSpy = vi.fn();
|
|
546
|
+
const customSkill: SkillObject = {
|
|
547
|
+
id: "use-hook-target-skill",
|
|
548
|
+
name: "Use Hook Target Skill",
|
|
549
|
+
spCost: 10,
|
|
550
|
+
hitRate: 1,
|
|
551
|
+
power: 0,
|
|
552
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
553
|
+
_type: "skill",
|
|
554
|
+
onUse: onUseSpy,
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const clientTesting2 = await fixture.createClient();
|
|
558
|
+
const targetPlayer = await clientTesting2.waitForMapChange("test-map");
|
|
559
|
+
|
|
560
|
+
player.learnSkill(customSkill);
|
|
561
|
+
player.useSkill("use-hook-target-skill", targetPlayer);
|
|
562
|
+
expect(onUseSpy).toHaveBeenCalledWith(player, targetPlayer);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("should call onUseFailed hook when hitRate fails", () => {
|
|
566
|
+
const onUseFailedSpy = vi.fn();
|
|
567
|
+
const customSkill: SkillObject = {
|
|
568
|
+
id: "use-failed-hook-skill",
|
|
569
|
+
name: "Use Failed Hook Skill",
|
|
570
|
+
spCost: 5,
|
|
571
|
+
hitRate: 0.1, // 10% chance
|
|
572
|
+
power: 0,
|
|
573
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
574
|
+
_type: "skill",
|
|
575
|
+
onUseFailed: onUseFailedSpy,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const originalRandom = Math.random;
|
|
579
|
+
Math.random = vi.fn(() => 0.9); // Will fail
|
|
580
|
+
|
|
581
|
+
player.learnSkill(customSkill);
|
|
582
|
+
try {
|
|
583
|
+
player.useSkill("use-failed-hook-skill");
|
|
584
|
+
} catch {
|
|
585
|
+
// Expected to throw
|
|
586
|
+
}
|
|
587
|
+
expect(onUseFailedSpy).toHaveBeenCalledWith(player, undefined);
|
|
588
|
+
|
|
589
|
+
Math.random = originalRandom;
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe("Skill Management - Edge Cases", () => {
|
|
594
|
+
test("should handle skill with default hitRate (1)", () => {
|
|
595
|
+
const skillWithoutHitRate: SkillObject = {
|
|
596
|
+
id: "no-hitrate-skill",
|
|
597
|
+
name: "No HitRate Skill",
|
|
598
|
+
spCost: 10,
|
|
599
|
+
power: 0,
|
|
600
|
+
coefficient: { [ATK]: 0, [PDEF]: 0 },
|
|
601
|
+
_type: "skill",
|
|
602
|
+
// No hitRate specified, should default to 1
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
player.learnSkill(skillWithoutHitRate);
|
|
606
|
+
const skill = player.useSkill("no-hitrate-skill");
|
|
607
|
+
expect(skill).toBeDefined();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("should handle skill with 0 SP cost", () => {
|
|
611
|
+
player.sp = 0;
|
|
612
|
+
player.learnSkill("free-skill");
|
|
613
|
+
const skill = player.useSkill("free-skill");
|
|
614
|
+
expect(skill).toBeDefined();
|
|
615
|
+
expect(player.sp).toBe(0);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("should handle multiple skill usages in sequence", () => {
|
|
619
|
+
player.sp = 50;
|
|
620
|
+
player.learnSkill("fire"); // costs 10
|
|
621
|
+
|
|
622
|
+
player.useSkill("fire");
|
|
623
|
+
expect(player.sp).toBe(40);
|
|
624
|
+
|
|
625
|
+
player.useSkill("fire");
|
|
626
|
+
expect(player.sp).toBe(30);
|
|
627
|
+
|
|
628
|
+
player.useSkill("fire");
|
|
629
|
+
expect(player.sp).toBe(20);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("should handle learning and forgetting same skill multiple times", () => {
|
|
633
|
+
player.learnSkill("fire");
|
|
634
|
+
player.forgetSkill("fire");
|
|
635
|
+
player.learnSkill("fire");
|
|
636
|
+
player.forgetSkill("fire");
|
|
637
|
+
player.learnSkill("fire");
|
|
638
|
+
|
|
639
|
+
expect(player.getSkill("fire")).toBeDefined();
|
|
640
|
+
expect(player.skills().length).toBe(1);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("should merge object properties when skill already in database", () => {
|
|
644
|
+
// First, learn fire from database
|
|
645
|
+
player.learnSkill("fire");
|
|
646
|
+
player.forgetSkill("fire");
|
|
647
|
+
|
|
648
|
+
// Now learn with modified properties
|
|
649
|
+
const modifiedFire: SkillObject = {
|
|
650
|
+
id: "fire",
|
|
651
|
+
name: "Modified Fire",
|
|
652
|
+
spCost: 5, // Changed from 10
|
|
653
|
+
};
|
|
654
|
+
const skill = player.learnSkill(modifiedFire);
|
|
655
|
+
expect(skill.name).toBe("Modified Fire");
|
|
656
|
+
expect(skill.spCost).toBe(5);
|
|
657
|
+
});
|
|
658
|
+
});
|