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