@rpgjs/server 5.0.0-alpha.24 → 5.0.0-alpha.26

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,591 @@
1
+ import { beforeEach, test, expect, afterEach, describe, vi } from "vitest";
2
+ import { testing, waitForSyncComplete } from "@rpgjs/testing";
3
+ import { defineModule, createModule } from "@rpgjs/common";
4
+ import { RpgPlayer, RpgServer } from "../src";
5
+ import { RpgClient } from "../../client/src";
6
+ import { ItemLog } from "../src/logs";
7
+ import type { ItemObject } from "../src/Player/ItemManager";
8
+
9
+ // Define test items as objects for database
10
+ const TestPotion = {
11
+ id: "TestPotion",
12
+ name: "Test Potion",
13
+ description: "Restores 100 HP",
14
+ price: 200,
15
+ hpValue: 100,
16
+ consumable: true,
17
+ _type: "item" as const,
18
+ };
19
+
20
+ const TestSword = {
21
+ name: "Test Sword",
22
+ description: "A basic sword",
23
+ price: 500,
24
+ atk: 50,
25
+ _type: "weapon" as const,
26
+ };
27
+
28
+ const TestArmor = {
29
+ name: "Test Armor",
30
+ description: "Basic armor",
31
+ price: 300,
32
+ pdef: 30,
33
+ sdef: 20,
34
+ _type: "armor" as const,
35
+ };
36
+
37
+ const TestNonConsumable = {
38
+ name: "Test Non-Consumable",
39
+ description: "Cannot be used",
40
+ price: 100,
41
+ consumable: false,
42
+ _type: "item" as const,
43
+ };
44
+
45
+ const TestExpensiveItem = {
46
+ name: "Expensive Item",
47
+ description: "Very expensive",
48
+ price: 10000,
49
+ _type: "item" as const,
50
+ };
51
+
52
+ const TestNoPriceItem = {
53
+ name: "No Price Item",
54
+ description: "Item without price",
55
+ _type: "item" as const,
56
+ };
57
+
58
+ let player: RpgPlayer;
59
+ let clientTesting: any;
60
+ let fixture: any;
61
+
62
+ // Define server module with items in database
63
+ const serverModule = defineModule<RpgServer>({
64
+ maps: [
65
+ {
66
+ id: "test-map",
67
+ file: "",
68
+ },
69
+ ],
70
+ database: {
71
+ TestPotion: TestPotion,
72
+ TestSword: TestSword,
73
+ TestArmor: TestArmor,
74
+ TestNonConsumable: TestNonConsumable,
75
+ TestExpensiveItem: TestExpensiveItem,
76
+ TestNoPriceItem: TestNoPriceItem,
77
+ },
78
+ player: {
79
+ async onConnected(player) {
80
+ await player.changeMap("test-map", { x: 100, y: 100 });
81
+ },
82
+ },
83
+ });
84
+
85
+ // Define client module
86
+ const clientModule = defineModule<RpgClient>({
87
+ // Client-side logic
88
+ });
89
+
90
+
91
+ beforeEach(async () => {
92
+
93
+ const myModule = createModule("TestModule", [
94
+ {
95
+ server: serverModule,
96
+ client: clientModule,
97
+ },
98
+ ]);
99
+
100
+ fixture = await testing(myModule);
101
+ clientTesting = await fixture.createClient();
102
+ player = await clientTesting.waitForMapChange("test-map");
103
+ });
104
+
105
+ afterEach(async () => {
106
+ await fixture.clear();
107
+ });
108
+
109
+
110
+ describe("Item Management - Basic Operations", () => {
111
+
112
+ test("should add item ", async () => {
113
+ const item = player.addItem(TestPotion, 5);
114
+ expect(item).toBeDefined();
115
+ expect(item.id()).toBe("TestPotion");
116
+ expect(item.quantity()).toBe(5);
117
+ expect(item.name()).toBe("Test Potion");
118
+ });
119
+
120
+ test("should add item using string ID", () => {
121
+ const item = player.addItem("TestPotion", 5);
122
+ expect(item).toBeDefined();
123
+ expect(item.id()).toBe("TestPotion");
124
+ expect(item.quantity()).toBe(5);
125
+ expect(item.name()).toBe("Test Potion");
126
+ });
127
+
128
+
129
+ test("should add item using object", () => {
130
+ const customItem: ItemObject = {
131
+ id: "custom-item",
132
+ name: "Custom Item",
133
+ description: "A custom item",
134
+ price: 150,
135
+ onAdd(player) {
136
+ // Hook test
137
+ },
138
+ };
139
+ const item = player.addItem(customItem, 3);
140
+ expect(item).toBeDefined();
141
+ expect(item.id()).toBe("custom-item");
142
+ expect(item.quantity()).toBe(3);
143
+ expect(item.name()).toBe("Custom Item");
144
+ });
145
+
146
+ test("should add item without ID (auto-generated)", () => {
147
+ const customItem: ItemObject = {
148
+ name: "Auto ID Item",
149
+ price: 100,
150
+ };
151
+ const item = player.addItem(customItem, 1);
152
+ expect(item).toBeDefined();
153
+ expect(item.id()).toMatch(/^item-\d+$/);
154
+ expect(item.name()).toBe("Auto ID Item");
155
+ });
156
+
157
+ test("should increment quantity when adding existing item", () => {
158
+ player.addItem("TestPotion", 3);
159
+ const item = player.addItem("TestPotion", 2);
160
+ expect(item.quantity()).toBe(5);
161
+ });
162
+
163
+ test("should add item when player is in Lobby (no map)", async () => {
164
+ const newFixture = await testing();
165
+ const newClient = await newFixture.createClient();
166
+ const newPlayer = newClient.player;
167
+
168
+ newPlayer.getCurrentMap()?.addInDatabase("TestPotion", TestPotion);
169
+ const item = newPlayer.addItem("TestPotion", 1);
170
+ expect(item).toBeDefined();
171
+ expect(item.id()).toBe("TestPotion");
172
+ expect(item.quantity()).toBe(1);
173
+ await newFixture.clear();
174
+ });
175
+
176
+ test("should throw error when adding item with invalid string ID", () => {
177
+ expect(() => {
178
+ player.addItem("NonExistentItem", 1);
179
+ }).toThrow("The ID=NonExistentItem data is not found in the database");
180
+ });
181
+
182
+ test("should get item from inventory", () => {
183
+ player.addItem("TestPotion", 5);
184
+ const item = player.getItem("TestPotion");
185
+ expect(item).toBeDefined();
186
+ expect(item.id()).toBe("TestPotion");
187
+ expect(item.quantity()).toBe(5);
188
+ });
189
+
190
+ test("should return undefined when getting non-existent item", () => {
191
+ const item = player.getItem("TestPotion");
192
+ expect(item).toBeUndefined();
193
+ });
194
+
195
+ test("should check if player has item", () => {
196
+ expect(player.hasItem("TestPotion")).toBe(false);
197
+ player.addItem("TestPotion", 1);
198
+ expect(player.hasItem("TestPotion")).toBe(true);
199
+ });
200
+
201
+ test("should remove item from inventory", () => {
202
+ player.addItem("TestPotion", 5);
203
+ const item = player.removeItem("TestPotion", 2);
204
+ expect(item).toBeDefined();
205
+ expect(item?.quantity()).toBe(3);
206
+ });
207
+
208
+ test("should remove item completely when quantity reaches zero", () => {
209
+ player.addItem("TestPotion", 2);
210
+ player.removeItem("TestPotion", 2);
211
+ expect(player.hasItem("TestPotion")).toBe(false);
212
+ const item = player.getItem("TestPotion");
213
+ expect(item).toBeUndefined();
214
+ });
215
+
216
+ test("should throw error when removing non-existent item", () => {
217
+ expect(() => {
218
+ player.removeItem("TestPotion", 1);
219
+ }).toThrow();
220
+ });
221
+ });
222
+
223
+ describe("Item Management - Buy and Sell", () => {
224
+ test("should buy item and reduce gold", () => {
225
+ player.gold = 1000;
226
+ const item = player.buyItem("TestPotion", 2);
227
+ expect(item).toBeDefined();
228
+ expect(item.quantity()).toBe(2);
229
+ expect(player.gold).toBe(600); // 1000 - (200 * 2)
230
+ });
231
+
232
+ test("should throw error when buying item without price", () => {
233
+ player.gold = 1000;
234
+ expect(() => {
235
+ player.buyItem("TestNoPriceItem", 1);
236
+ }).toThrow();
237
+ });
238
+
239
+ test("should throw error when not enough gold", () => {
240
+ player.gold = 100;
241
+ expect(() => {
242
+ player.buyItem("TestExpensiveItem", 1);
243
+ }).toThrow();
244
+ });
245
+
246
+ test("should sell item and increase gold", () => {
247
+ player.gold = 1000;
248
+ player.addItem("TestPotion", 3);
249
+ const item = player.sellItem("TestPotion", 2);
250
+ expect(item).toBeDefined();
251
+ expect(player.gold).toBe(1200); // 1000 + (200 / 2 * 2)
252
+ expect(player.getItem("TestPotion")?.quantity()).toBe(1);
253
+ });
254
+
255
+ test("should throw error when selling non-existent item", () => {
256
+ expect(() => {
257
+ player.sellItem("TestPotion", 1);
258
+ }).toThrow();
259
+ });
260
+
261
+ test("should throw error when selling more items than available", () => {
262
+ player.addItem("TestPotion", 2);
263
+ expect(() => {
264
+ player.sellItem("TestPotion", 5);
265
+ }).toThrow();
266
+ });
267
+
268
+ test("should throw error when selling item without price", () => {
269
+ player.addItem("TestNoPriceItem", 1);
270
+ expect(() => {
271
+ player.sellItem("TestNoPriceItem", 1);
272
+ }).toThrow();
273
+ });
274
+ });
275
+
276
+ describe("Item Management - Use Item", () => {
277
+ test("should use consumable item", () => {
278
+ const initialHp = player.hp;
279
+ player.addItem("TestPotion", 1);
280
+ const item = player.useItem("TestPotion");
281
+ expect(item).toBeDefined();
282
+ expect(player.hasItem("TestPotion")).toBe(false);
283
+ // Note: applyEffect might not be implemented in test environment
284
+ });
285
+
286
+ test("should throw error when using non-existent item", () => {
287
+ expect(() => {
288
+ player.useItem("TestPotion");
289
+ }).toThrow();
290
+ });
291
+
292
+ test("should throw error when using non-consumable item", () => {
293
+ player.addItem("TestNonConsumable", 1);
294
+ expect(() => {
295
+ player.useItem("TestNonConsumable");
296
+ }).toThrow();
297
+ });
298
+
299
+ test("should handle hitRate chance (success)", () => {
300
+ // Mock Math.random to return a value that passes hitRate
301
+ const originalRandom = Math.random;
302
+ Math.random = vi.fn(() => 0.5); // 0.5 < 1.0 (default hitRate)
303
+
304
+ player.addItem("TestPotion", 1);
305
+ const item = player.useItem("TestPotion");
306
+ expect(item).toBeDefined();
307
+ expect(player.hasItem("TestPotion")).toBe(false);
308
+
309
+ Math.random = originalRandom;
310
+ });
311
+
312
+ test("should handle hitRate chance (failure)", () => {
313
+ // Create a potion with low hitRate (10% chance)
314
+ const lowHitRatePotion: ItemObject = {
315
+ id: "LowHitRatePotion",
316
+ name: "Low Hit Rate Potion",
317
+ price: 200,
318
+ consumable: true,
319
+ hitRate: 0.1, // 10% chance
320
+ };
321
+ player.getCurrentMap()?.addInDatabase("LowHitRatePotion", lowHitRatePotion);
322
+
323
+ // Mock Math.random to return a value that fails hitRate
324
+ const originalRandom = Math.random;
325
+ Math.random = vi.fn(() => 0.9); // 0.9 > 0.1 (hitRate)
326
+
327
+ player.addItem("LowHitRatePotion", 1);
328
+ expect(() => {
329
+ player.useItem("LowHitRatePotion");
330
+ }).toThrow();
331
+
332
+ // Item should still be removed even on failure
333
+ expect(player.hasItem("LowHitRatePotion")).toBe(false);
334
+
335
+ Math.random = originalRandom;
336
+ });
337
+
338
+ test("should use item with custom hitRate", () => {
339
+ const customItem: ItemObject = {
340
+ id: "chance-item",
341
+ name: "Chance Item",
342
+ price: 100,
343
+ consumable: true,
344
+ hitRate: 0.5, // 50% chance
345
+ };
346
+ player.addItem(customItem, 1);
347
+
348
+ // Mock Math.random to pass
349
+ const originalRandom = Math.random;
350
+ Math.random = vi.fn(() => 0.3); // 0.3 < 0.5
351
+
352
+ const item = player.useItem("chance-item");
353
+ expect(item).toBeDefined();
354
+
355
+ Math.random = originalRandom;
356
+ });
357
+ });
358
+
359
+ describe("Item Management - Equipment", () => {
360
+ test("should equip weapon", () => {
361
+ player.addItem("TestSword", 1);
362
+ player.equip("TestSword", true);
363
+ const item = player.getItem("TestSword");
364
+ expect((item as any).equipped).toBe(true);
365
+ expect(player.equipments().some((eq) => eq.id() === "TestSword")).toBe(
366
+ true
367
+ );
368
+ });
369
+
370
+ test("should unequip weapon", () => {
371
+ player.addItem("TestSword", 1);
372
+ player.equip("TestSword", true);
373
+ player.equip("TestSword", false);
374
+ const item = player.getItem("TestSword");
375
+ expect((item as any).equipped).toBe(false);
376
+ expect(player.equipments().some((eq) => eq.id() === "TestSword")).toBe(
377
+ false
378
+ );
379
+ });
380
+
381
+ test("should equip armor", () => {
382
+ player.addItem("TestArmor", 1);
383
+ player.equip("TestArmor", true);
384
+ const item = player.getItem("TestArmor");
385
+ expect((item as any).equipped).toBe(true);
386
+ });
387
+
388
+ test("should throw error when equipping non-existent item", () => {
389
+ expect(() => {
390
+ player.equip("TestSword", true);
391
+ }).toThrow();
392
+ });
393
+
394
+ test("should throw error when equipping regular item (not weapon/armor)", () => {
395
+ player.addItem("TestPotion", 1);
396
+ expect(() => {
397
+ player.equip("TestPotion", true);
398
+ }).toThrow();
399
+ });
400
+
401
+ test("should throw error when equipping already equipped item", () => {
402
+ player.addItem("TestSword", 1);
403
+ player.equip("TestSword", true);
404
+ expect(() => {
405
+ player.equip("TestSword", true);
406
+ }).toThrow();
407
+ });
408
+ });
409
+
410
+ describe("Item Management - Parameters (ATK, PDEF, SDEF)", () => {
411
+ test("should calculate attack from equipped weapon", () => {
412
+ player.addItem("TestSword", 1);
413
+ player.equip("TestSword", true);
414
+ expect(player.atk).toBe(50);
415
+ });
416
+
417
+ test("should calculate physical defense from equipped armor", () => {
418
+ player.addItem("TestArmor", 1);
419
+ player.equip("TestArmor", true);
420
+ expect(player.pdef).toBe(30);
421
+ });
422
+
423
+ test("should calculate skill defense from equipped armor", () => {
424
+ player.addItem("TestArmor", 1);
425
+ player.equip("TestArmor", true);
426
+ expect(player.sdef).toBe(20);
427
+ });
428
+
429
+ test("should return 0 for parameters when no equipment", () => {
430
+ expect(player.atk).toBe(0);
431
+ expect(player.pdef).toBe(0);
432
+ expect(player.sdef).toBe(0);
433
+ });
434
+
435
+ test("should sum parameters from multiple equipped items", () => {
436
+ const sword2: ItemObject & { atk: number } = {
437
+ id: "sword2",
438
+ name: "Sword 2",
439
+ price: 600,
440
+ atk: 75,
441
+ _type: "weapon" as const,
442
+ };
443
+ player.getCurrentMap()?.addInDatabase("sword2", sword2);
444
+
445
+ player.addItem("TestSword", 1);
446
+ player.addItem("sword2", 1);
447
+ player.equip("TestSword", true);
448
+ player.equip("sword2", true);
449
+
450
+ // Only one weapon should be equipped at a time typically, but test the sum
451
+ expect(player.atk).toBeGreaterThanOrEqual(50);
452
+ });
453
+ });
454
+
455
+ describe("Item Management - Hooks", () => {
456
+ test("should call onAdd hook when adding item", () => {
457
+ const onAddSpy = vi.fn();
458
+ const customItem: ItemObject = {
459
+ id: "hook-item",
460
+ name: "Hook Item",
461
+ price: 100,
462
+ onAdd: onAddSpy,
463
+ };
464
+
465
+ player.addItem(customItem, 1);
466
+ expect(onAddSpy).toHaveBeenCalledWith(player);
467
+ });
468
+
469
+ test("should call onRemove hook when removing item", () => {
470
+ const onRemoveSpy = vi.fn();
471
+ const customItem: ItemObject = {
472
+ id: "remove-hook-item",
473
+ name: "Remove Hook Item",
474
+ price: 100,
475
+ onRemove: onRemoveSpy,
476
+ };
477
+
478
+ player.addItem(customItem, 1);
479
+ player.removeItem("remove-hook-item", 1);
480
+ expect(onRemoveSpy).toHaveBeenCalledWith(player);
481
+ });
482
+
483
+ test("should call onUse hook when using item", () => {
484
+ const onUseSpy = vi.fn();
485
+ const customItem: ItemObject = {
486
+ id: "use-hook-item",
487
+ name: "Use Hook Item",
488
+ price: 100,
489
+ consumable: true,
490
+ onUse: onUseSpy,
491
+ };
492
+
493
+ // Mock Math.random to ensure success
494
+ const originalRandom = Math.random;
495
+ Math.random = vi.fn(() => 0.5);
496
+
497
+ player.addItem(customItem, 1);
498
+ player.useItem("use-hook-item");
499
+ expect(onUseSpy).toHaveBeenCalledWith(player);
500
+
501
+ Math.random = originalRandom;
502
+ });
503
+
504
+ test("should call onUseFailed hook when item usage fails", () => {
505
+ const onUseFailedSpy = vi.fn();
506
+ const customItem: ItemObject = {
507
+ id: "fail-hook-item",
508
+ name: "Fail Hook Item",
509
+ price: 100,
510
+ consumable: true,
511
+ hitRate: 0.1, // 10% chance
512
+ onUseFailed: onUseFailedSpy,
513
+ };
514
+
515
+ // Mock Math.random to fail
516
+ const originalRandom = Math.random;
517
+ Math.random = vi.fn(() => 0.9); // 0.9 > 0.1
518
+
519
+ player.addItem(customItem, 1);
520
+ try {
521
+ player.useItem("fail-hook-item");
522
+ } catch (e) {
523
+ // Expected to throw
524
+ }
525
+ expect(onUseFailedSpy).toHaveBeenCalledWith(player);
526
+
527
+ Math.random = originalRandom;
528
+ });
529
+
530
+ test("should call onEquip hook when equipping item", () => {
531
+ const onEquipSpy = vi.fn();
532
+ const customWeapon: ItemObject & { atk: number } = {
533
+ id: "equip-hook-weapon",
534
+ name: "Equip Hook Weapon",
535
+ price: 500,
536
+ atk: 40,
537
+ _type: "weapon" as const,
538
+ onEquip: onEquipSpy,
539
+ };
540
+
541
+ player.addItem(customWeapon, 1);
542
+ player.equip("equip-hook-weapon", true);
543
+ expect(onEquipSpy).toHaveBeenCalledWith(player, true);
544
+
545
+ player.equip("equip-hook-weapon", false);
546
+ expect(onEquipSpy).toHaveBeenCalledWith(player, false);
547
+ });
548
+ });
549
+
550
+ describe("Item Management - Edge Cases", () => {
551
+ test("should handle adding item with class (if supported)", () => {
552
+ // This test would require an actual Item class
553
+ // For now, we test that object items work
554
+ const itemObj: ItemObject = {
555
+ id: "class-like-item",
556
+ name: "Class Like Item",
557
+ price: 100,
558
+ };
559
+ const item = player.addItem(itemObj, 1);
560
+ expect(item).toBeDefined();
561
+ });
562
+
563
+ test("should handle merging existing item data with new object", () => {
564
+ // Add item first
565
+ player.addItem("TestPotion", 1);
566
+
567
+ // Add same item with different properties
568
+ const updatedItem: ItemObject = {
569
+ id: "TestPotion",
570
+ name: "Updated Potion",
571
+ price: 250,
572
+ };
573
+ const item = player.addItem(updatedItem, 1);
574
+ expect(item.name()).toBe("Updated Potion");
575
+ expect(item.quantity()).toBe(2);
576
+ });
577
+
578
+ test("should handle multiple items in inventory", () => {
579
+ player.addItem("TestPotion", 3);
580
+ player.addItem("TestSword", 1);
581
+ player.addItem("TestArmor", 2);
582
+
583
+ expect(player.hasItem("TestPotion")).toBe(true);
584
+ expect(player.hasItem("TestSword")).toBe(true);
585
+ expect(player.hasItem("TestArmor")).toBe(true);
586
+
587
+ expect(player.getItem("TestPotion")?.quantity()).toBe(3);
588
+ expect(player.getItem("TestSword")?.quantity()).toBe(1);
589
+ expect(player.getItem("TestArmor")?.quantity()).toBe(2);
590
+ });
591
+ });
@@ -0,0 +1,38 @@
1
+ import { beforeEach, test, expect } from 'vitest'
2
+ import { testing } from '@rpgjs/testing'
3
+ import { defineModule, createModule } from '@rpgjs/common'
4
+ import { RpgPlayer, RpgServer } from '../src'
5
+ import { RpgClient } from '../../client/src'
6
+
7
+ // Define your server module
8
+ const serverModule = defineModule<RpgServer>({
9
+ player: {
10
+ onConnected(player) {
11
+ player.setVariable('test', 'value')
12
+ }
13
+ }
14
+ })
15
+
16
+ // Define your client module
17
+ const clientModule = defineModule<RpgClient>({
18
+ // Client-side logic
19
+ })
20
+
21
+ let player: RpgPlayer
22
+
23
+ beforeEach(async () => {
24
+ // Create module using createModule
25
+ const myModule = createModule('MyModule', [{
26
+ server: serverModule,
27
+ client: clientModule
28
+ }])
29
+
30
+ // Pass the module created with createModule to testing
31
+ const fixture = await testing(myModule)
32
+ const client = await fixture.createClient()
33
+ player = client.player
34
+ })
35
+
36
+ test('Module hook was called', () => {
37
+ expect(player.getVariable('test')).toBe('value')
38
+ })
@@ -0,0 +1,28 @@
1
+
2
+ import { testing } from '@rpgjs/testing'
3
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
4
+ import { ATK, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, RpgPlayer } from '../src'
5
+
6
+ let player: RpgPlayer
7
+
8
+ beforeEach(async () => {
9
+ const fixture = await testing();
10
+ const client = await fixture.createClient()
11
+ player = client.player
12
+ })
13
+
14
+ test('Test HP', () => {
15
+ expect(player.hp).toBe(MAXHP_CURVE.start)
16
+ })
17
+
18
+ test('Test SP', () => {
19
+ expect(player.sp).toBe(MAXSP_CURVE.start)
20
+ })
21
+
22
+ test('Test MaxHP', () => {
23
+ expect(player.param[MAXHP]).toBe(MAXHP_CURVE.start)
24
+ })
25
+
26
+ test('Test MaxSP', () => {
27
+ expect(player.param[MAXSP]).toBe(MAXSP_CURVE.start)
28
+ })