@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.
@@ -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
+ });