@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,467 @@
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 } from "../src";
5
+
6
+ /**
7
+ * Test state class - Poison
8
+ *
9
+ * A basic debuff state that damages over time.
10
+ */
11
+ class PoisonState {
12
+ static id = "poison";
13
+ id = "poison";
14
+ name = "Poison";
15
+ description = "Takes damage over time";
16
+ effects = [];
17
+
18
+ // States can add/remove other states when applied
19
+ addStates = [];
20
+ removeStates = [];
21
+ }
22
+
23
+ /**
24
+ * Test state class - Paralysis
25
+ *
26
+ * A state that prevents actions.
27
+ */
28
+ class ParalysisState {
29
+ static id = "paralysis";
30
+ id = "paralysis";
31
+ name = "Paralysis";
32
+ description = "Cannot move or act";
33
+ effects = ["CAN_NOT_SKILL"];
34
+
35
+ addStates = [];
36
+ removeStates = [];
37
+ }
38
+
39
+ /**
40
+ * Test state class - Berserk
41
+ *
42
+ * A state that increases attack but reduces defense.
43
+ */
44
+ class BerserkState {
45
+ static id = "berserk";
46
+ id = "berserk";
47
+ name = "Berserk";
48
+ description = "Increased attack, reduced defense";
49
+ effects = [];
50
+
51
+ addStates = [];
52
+ removeStates = [];
53
+ }
54
+
55
+ /**
56
+ * Test state class - Regeneration
57
+ *
58
+ * A state that heals over time.
59
+ */
60
+ class RegenerationState {
61
+ static id = "regeneration";
62
+ id = "regeneration";
63
+ name = "Regeneration";
64
+ description = "Heals over time";
65
+ effects = [];
66
+
67
+ addStates = [];
68
+ removeStates = [];
69
+ }
70
+
71
+ /**
72
+ * Test state class - Shield
73
+ *
74
+ * A state that provides defense.
75
+ */
76
+ class ShieldState {
77
+ static id = "shield";
78
+ id = "shield";
79
+ name = "Shield";
80
+ description = "Provides defense boost";
81
+ effects = ["GUARD"];
82
+
83
+ addStates = [];
84
+ removeStates = [];
85
+ }
86
+
87
+ /**
88
+ * State object for database
89
+ */
90
+ const PoisonStateData = {
91
+ id: "poison",
92
+ name: "Poison",
93
+ description: "Takes damage over time",
94
+ effects: [],
95
+ _type: "state" as const,
96
+ };
97
+
98
+ const ParalysisStateData = {
99
+ id: "paralysis",
100
+ name: "Paralysis",
101
+ description: "Cannot move or act",
102
+ effects: ["CAN_NOT_SKILL"],
103
+ _type: "state" as const,
104
+ };
105
+
106
+ const BerserkStateData = {
107
+ id: "berserk",
108
+ name: "Berserk",
109
+ description: "Increased attack, reduced defense",
110
+ effects: [],
111
+ _type: "state" as const,
112
+ };
113
+
114
+ let player: RpgPlayer;
115
+ let fixture: TestingFixture;
116
+
117
+ // Define server module with states in database
118
+ const serverModule = defineModule({
119
+ maps: [
120
+ {
121
+ id: "test-map",
122
+ file: "",
123
+ },
124
+ ],
125
+ database: {
126
+ poison: PoisonState,
127
+ paralysis: ParalysisState,
128
+ berserk: BerserkState,
129
+ regeneration: RegenerationState,
130
+ shield: ShieldState,
131
+ },
132
+ player: {
133
+ async onConnected(player) {
134
+ await player.changeMap("test-map", { x: 100, y: 100 });
135
+ },
136
+ },
137
+ });
138
+
139
+ // Define client module
140
+ const clientModule = defineModule({
141
+ // Client-side logic
142
+ });
143
+
144
+ beforeEach(async () => {
145
+ const myModule = createModule("TestModule", [
146
+ {
147
+ server: serverModule,
148
+ client: clientModule,
149
+ },
150
+ ]);
151
+
152
+ fixture = await testing(myModule);
153
+ const clientTesting = await fixture.createClient();
154
+ player = await clientTesting.waitForMapChange("test-map");
155
+ });
156
+
157
+ afterEach(async () => {
158
+ await fixture.clear();
159
+ });
160
+
161
+ describe("State Management - Adding States", () => {
162
+ test("should add a state using class", () => {
163
+ const state = player.addState(PoisonState);
164
+ expect(state).toBeDefined();
165
+ expect(state?.id).toBe("poison");
166
+ });
167
+
168
+ test("should add a state using string ID", () => {
169
+ const state = player.addState("poison");
170
+ expect(state).toBeDefined();
171
+ expect(state?.id).toBe("poison");
172
+ });
173
+
174
+ test("should return null when adding already applied state", () => {
175
+ player.addState(PoisonState);
176
+ const result = player.addState(PoisonState);
177
+ expect(result).toBeNull();
178
+ });
179
+
180
+ test("should add multiple different states", () => {
181
+ player.addState(PoisonState);
182
+ player.addState(ParalysisState);
183
+ player.addState(BerserkState);
184
+
185
+ expect(player.getState(PoisonState)).toBeDefined();
186
+ expect(player.getState(ParalysisState)).toBeDefined();
187
+ expect(player.getState(BerserkState)).toBeDefined();
188
+ expect(player.states().length).toBe(3);
189
+ });
190
+
191
+ test("should throw error when chance roll fails", () => {
192
+ const originalRandom = Math.random;
193
+ Math.random = vi.fn(() => 0.9); // 0.9 > 0.5 (chance)
194
+
195
+ expect(() => {
196
+ player.addState(PoisonState, 0.5);
197
+ }).toThrow();
198
+
199
+ Math.random = originalRandom;
200
+ });
201
+
202
+ test("should succeed when chance roll passes", () => {
203
+ const originalRandom = Math.random;
204
+ Math.random = vi.fn(() => 0.3); // 0.3 < 0.5 (chance)
205
+
206
+ const state = player.addState(PoisonState, 0.5);
207
+ expect(state).toBeDefined();
208
+
209
+ Math.random = originalRandom;
210
+ });
211
+
212
+ test("should always succeed with chance of 1", () => {
213
+ const originalRandom = Math.random;
214
+ Math.random = vi.fn(() => 0.99);
215
+
216
+ const state = player.addState(PoisonState, 1);
217
+ expect(state).toBeDefined();
218
+
219
+ Math.random = originalRandom;
220
+ });
221
+ });
222
+
223
+ describe("State Management - Getting States", () => {
224
+ test("should get applied state by class", () => {
225
+ player.addState(PoisonState);
226
+ const state = player.getState(PoisonState);
227
+ expect(state).toBeDefined();
228
+ expect(state.id).toBe("poison");
229
+ });
230
+
231
+ test("should get applied state by string ID", () => {
232
+ player.addState("poison");
233
+ const state = player.getState("poison");
234
+ expect(state).toBeDefined();
235
+ expect(state.id).toBe("poison");
236
+ });
237
+
238
+ test("should return undefined for non-applied state", () => {
239
+ const state = player.getState(PoisonState);
240
+ expect(state).toBeUndefined();
241
+ });
242
+
243
+ test("should return undefined for non-applied state by string ID", () => {
244
+ const state = player.getState("poison");
245
+ expect(state).toBeUndefined();
246
+ });
247
+ });
248
+
249
+ describe("State Management - Removing States", () => {
250
+ test("should remove an applied state by class", () => {
251
+ player.addState(PoisonState);
252
+ player.removeState(PoisonState);
253
+ expect(player.getState(PoisonState)).toBeUndefined();
254
+ });
255
+
256
+ test("should remove an applied state by string ID", () => {
257
+ player.addState("poison");
258
+ player.removeState("poison");
259
+ expect(player.getState("poison")).toBeUndefined();
260
+ });
261
+
262
+ test("should throw error when removing non-applied state", () => {
263
+ expect(() => {
264
+ player.removeState(PoisonState);
265
+ }).toThrow();
266
+ });
267
+
268
+ test("should throw error when removing non-applied state by string ID", () => {
269
+ expect(() => {
270
+ player.removeState("poison");
271
+ }).toThrow();
272
+ });
273
+
274
+ test("should throw error when chance roll fails for removal", () => {
275
+ const originalRandom = Math.random;
276
+
277
+ // First add the state (make random pass)
278
+ Math.random = vi.fn(() => 0.1);
279
+ player.addState(PoisonState);
280
+
281
+ // Now try to remove with failing chance
282
+ Math.random = vi.fn(() => 0.9); // 0.9 > 0.5 (chance)
283
+
284
+ expect(() => {
285
+ player.removeState(PoisonState, 0.5);
286
+ }).toThrow();
287
+
288
+ Math.random = originalRandom;
289
+ });
290
+
291
+ test("should succeed removal when chance roll passes", () => {
292
+ const originalRandom = Math.random;
293
+
294
+ Math.random = vi.fn(() => 0.1);
295
+ player.addState(PoisonState);
296
+
297
+ Math.random = vi.fn(() => 0.3); // 0.3 < 0.5 (chance)
298
+ player.removeState(PoisonState, 0.5);
299
+
300
+ expect(player.getState(PoisonState)).toBeUndefined();
301
+
302
+ Math.random = originalRandom;
303
+ });
304
+
305
+ test("should be able to re-add state after removal", () => {
306
+ player.addState(PoisonState);
307
+ player.removeState(PoisonState);
308
+ const state = player.addState(PoisonState);
309
+ expect(state).toBeDefined();
310
+ expect(player.getState(PoisonState)).toBeDefined();
311
+ });
312
+ });
313
+
314
+ describe("State Management - Apply States (Batch)", () => {
315
+ let targetPlayer: RpgPlayer;
316
+
317
+ beforeEach(async () => {
318
+ const clientTesting2 = await fixture.createClient();
319
+ targetPlayer = await clientTesting2.waitForMapChange("test-map");
320
+ });
321
+
322
+ test("should apply multiple states to target player", () => {
323
+ const originalRandom = Math.random;
324
+ Math.random = vi.fn(() => 0.1);
325
+
326
+ player.applyStates(targetPlayer, {
327
+ addStates: [
328
+ { state: PoisonState, rate: 1 },
329
+ { state: ParalysisState, rate: 1 },
330
+ ],
331
+ });
332
+
333
+ expect(targetPlayer.getState(PoisonState)).toBeDefined();
334
+ expect(targetPlayer.getState(ParalysisState)).toBeDefined();
335
+
336
+ Math.random = originalRandom;
337
+ });
338
+
339
+ test("should remove multiple states from target player", () => {
340
+ const originalRandom = Math.random;
341
+ Math.random = vi.fn(() => 0.1);
342
+
343
+ // First add the states
344
+ targetPlayer.addState(PoisonState);
345
+ targetPlayer.addState(ParalysisState);
346
+
347
+ // Then remove them via applyStates
348
+ player.applyStates(targetPlayer, {
349
+ removeStates: [
350
+ { state: PoisonState, rate: 1 },
351
+ { state: ParalysisState, rate: 1 },
352
+ ],
353
+ });
354
+
355
+ expect(targetPlayer.getState(PoisonState)).toBeUndefined();
356
+ expect(targetPlayer.getState(ParalysisState)).toBeUndefined();
357
+
358
+ Math.random = originalRandom;
359
+ });
360
+
361
+ test("should add and remove states in same call", () => {
362
+ const originalRandom = Math.random;
363
+ Math.random = vi.fn(() => 0.1);
364
+
365
+ // First add a state to remove
366
+ targetPlayer.addState(PoisonState);
367
+
368
+ // Apply: add Berserk, remove Poison
369
+ player.applyStates(targetPlayer, {
370
+ addStates: [{ state: BerserkState, rate: 1 }],
371
+ removeStates: [{ state: PoisonState, rate: 1 }],
372
+ });
373
+
374
+ expect(targetPlayer.getState(PoisonState)).toBeUndefined();
375
+ expect(targetPlayer.getState(BerserkState)).toBeDefined();
376
+
377
+ Math.random = originalRandom;
378
+ });
379
+
380
+ test("should handle empty addStates and removeStates", () => {
381
+ player.applyStates(targetPlayer, {});
382
+ expect(targetPlayer.states().length).toBe(0);
383
+ });
384
+ });
385
+
386
+ describe("State Management - State Effects", () => {
387
+ test("should have effects from state", () => {
388
+ player.addState(ParalysisState);
389
+ const state = player.getState(ParalysisState);
390
+ expect(state.effects).toContain("CAN_NOT_SKILL");
391
+ });
392
+
393
+ test("should have effects from shield state", () => {
394
+ player.addState(ShieldState);
395
+ const state = player.getState(ShieldState);
396
+ expect(state.effects).toContain("GUARD");
397
+ });
398
+ });
399
+
400
+ describe("State Management - Edge Cases", () => {
401
+ test("should handle adding and removing same state multiple times", () => {
402
+ player.addState(PoisonState);
403
+ player.removeState(PoisonState);
404
+ player.addState(PoisonState);
405
+ player.removeState(PoisonState);
406
+ player.addState(PoisonState);
407
+
408
+ expect(player.getState(PoisonState)).toBeDefined();
409
+ expect(player.states().length).toBe(1);
410
+ });
411
+
412
+ test("should maintain state list integrity after operations", () => {
413
+ player.addState(PoisonState);
414
+ player.addState(ParalysisState);
415
+ player.addState(BerserkState);
416
+
417
+ expect(player.states().length).toBe(3);
418
+
419
+ player.removeState(ParalysisState);
420
+ expect(player.states().length).toBe(2);
421
+ expect(player.getState(PoisonState)).toBeDefined();
422
+ expect(player.getState(ParalysisState)).toBeUndefined();
423
+ expect(player.getState(BerserkState)).toBeDefined();
424
+ });
425
+
426
+ test("should handle state with 0 chance (always fail)", () => {
427
+ expect(() => {
428
+ player.addState(PoisonState, 0);
429
+ }).toThrow();
430
+ });
431
+
432
+ test("should work with both class and string ID for same state", () => {
433
+ // Add with class
434
+ player.addState(PoisonState);
435
+
436
+ // Get with string ID
437
+ const stateByString = player.getState("poison");
438
+ expect(stateByString).toBeDefined();
439
+
440
+ // Remove with class
441
+ player.removeState(PoisonState);
442
+ expect(player.getState("poison")).toBeUndefined();
443
+ });
444
+ });
445
+
446
+ describe("State Management - State Efficiency", () => {
447
+ test("should have empty statesEfficiency by default", () => {
448
+ expect(player.statesEfficiency()).toEqual([]);
449
+ });
450
+
451
+ test("should be able to set statesEfficiency", () => {
452
+ const efficiencies = [{ state: PoisonState, rate: 0.5 }];
453
+ player.statesEfficiency = efficiencies as any;
454
+ expect(player.statesEfficiency).toBeDefined();
455
+ });
456
+
457
+ test("should find state efficiency", () => {
458
+ // Set up statesEfficiency
459
+ player._statesEfficiency.set([{ state: new PoisonState(), rate: 0.5 }]);
460
+
461
+ // This test depends on how findStateEfficiency is implemented
462
+ // Currently it uses instanceof, so we need to check if it finds the efficiency
463
+ const efficiency = player.findStateEfficiency(PoisonState);
464
+ expect(efficiency).toBeDefined();
465
+ });
466
+ });
467
+
@@ -0,0 +1,185 @@
1
+ import { beforeEach, test, expect, afterEach, describe } from "vitest";
2
+ import { testing, TestingFixture } from "@rpgjs/testing";
3
+ import { defineModule, createModule } from "@rpgjs/common";
4
+ import { RpgPlayer } from "../src";
5
+
6
+ let player: RpgPlayer;
7
+ let fixture: TestingFixture;
8
+
9
+ const serverModule = defineModule({
10
+ maps: [{ id: "test-map", file: "" }],
11
+ player: {
12
+ async onConnected(player) {
13
+ await player.changeMap("test-map", { x: 100, y: 100 });
14
+ },
15
+ },
16
+ });
17
+
18
+ const clientModule = defineModule({});
19
+
20
+ beforeEach(async () => {
21
+ const myModule = createModule("TestModule", [
22
+ { server: serverModule, client: clientModule },
23
+ ]);
24
+ fixture = await testing(myModule);
25
+ const clientTesting = await fixture.createClient();
26
+ player = await clientTesting.waitForMapChange("test-map");
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await fixture.clear();
31
+ });
32
+
33
+ describe("Variable Manager - setVariable and getVariable", () => {
34
+ test("should set and get a string variable", () => {
35
+ player.setVariable("name", "John");
36
+ expect(player.getVariable("name")).toBe("John");
37
+ });
38
+
39
+ test("should set and get a number variable", () => {
40
+ player.setVariable("score", 100);
41
+ expect(player.getVariable("score")).toBe(100);
42
+ });
43
+
44
+ test("should set and get a boolean variable", () => {
45
+ player.setVariable("questCompleted", true);
46
+ expect(player.getVariable("questCompleted")).toBe(true);
47
+ });
48
+
49
+ test("should set and get an object variable", () => {
50
+ const questData = { id: 1, progress: 50 };
51
+ player.setVariable("currentQuest", questData);
52
+ expect(player.getVariable("currentQuest")).toEqual(questData);
53
+ });
54
+
55
+ test("should set and get an array variable", () => {
56
+ const inventory = ["sword", "shield", "potion"];
57
+ player.setVariable("inventory", inventory);
58
+ expect(player.getVariable("inventory")).toEqual(inventory);
59
+ });
60
+
61
+ test("should return undefined for non-existent variable", () => {
62
+ expect(player.getVariable("nonExistent")).toBeUndefined();
63
+ });
64
+
65
+ test("should overwrite existing variable", () => {
66
+ player.setVariable("score", 100);
67
+ player.setVariable("score", 200);
68
+ expect(player.getVariable("score")).toBe(200);
69
+ });
70
+ });
71
+
72
+ describe("Variable Manager - hasVariable", () => {
73
+ test("should return true for existing variable", () => {
74
+ player.setVariable("exists", true);
75
+ expect(player.hasVariable("exists")).toBe(true);
76
+ });
77
+
78
+ test("should return false for non-existing variable", () => {
79
+ expect(player.hasVariable("notExists")).toBe(false);
80
+ });
81
+
82
+ test("should return true even for null or undefined values", () => {
83
+ player.setVariable("nullVar", null);
84
+ expect(player.hasVariable("nullVar")).toBe(true);
85
+
86
+ player.setVariable("undefinedVar", undefined);
87
+ expect(player.hasVariable("undefinedVar")).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe("Variable Manager - removeVariable", () => {
92
+ test("should remove an existing variable", () => {
93
+ player.setVariable("toRemove", "value");
94
+ const result = player.removeVariable("toRemove");
95
+ expect(result).toBe(true);
96
+ expect(player.hasVariable("toRemove")).toBe(false);
97
+ });
98
+
99
+ test("should return false when removing non-existing variable", () => {
100
+ const result = player.removeVariable("notExists");
101
+ expect(result).toBe(false);
102
+ });
103
+
104
+ test("should allow re-adding removed variable", () => {
105
+ player.setVariable("temp", "first");
106
+ player.removeVariable("temp");
107
+ player.setVariable("temp", "second");
108
+ expect(player.getVariable("temp")).toBe("second");
109
+ });
110
+ });
111
+
112
+ describe("Variable Manager - getVariableKeys", () => {
113
+ test("should return empty array when no variables", () => {
114
+ expect(player.getVariableKeys()).toEqual([]);
115
+ });
116
+
117
+ test("should return all variable keys", () => {
118
+ player.setVariable("key1", "value1");
119
+ player.setVariable("key2", "value2");
120
+ player.setVariable("key3", "value3");
121
+
122
+ const keys = player.getVariableKeys();
123
+ expect(keys).toContain("key1");
124
+ expect(keys).toContain("key2");
125
+ expect(keys).toContain("key3");
126
+ expect(keys.length).toBe(3);
127
+ });
128
+
129
+ test("should not include removed keys", () => {
130
+ player.setVariable("key1", "value1");
131
+ player.setVariable("key2", "value2");
132
+ player.removeVariable("key1");
133
+
134
+ const keys = player.getVariableKeys();
135
+ expect(keys).not.toContain("key1");
136
+ expect(keys).toContain("key2");
137
+ });
138
+ });
139
+
140
+ describe("Variable Manager - clearVariables", () => {
141
+ test("should clear all variables", () => {
142
+ player.setVariable("key1", "value1");
143
+ player.setVariable("key2", "value2");
144
+ player.setVariable("key3", "value3");
145
+
146
+ player.clearVariables();
147
+
148
+ expect(player.getVariableKeys()).toEqual([]);
149
+ expect(player.hasVariable("key1")).toBe(false);
150
+ expect(player.hasVariable("key2")).toBe(false);
151
+ expect(player.hasVariable("key3")).toBe(false);
152
+ });
153
+
154
+ test("should allow adding variables after clear", () => {
155
+ player.setVariable("old", "oldValue");
156
+ player.clearVariables();
157
+ player.setVariable("new", "newValue");
158
+
159
+ expect(player.getVariable("old")).toBeUndefined();
160
+ expect(player.getVariable("new")).toBe("newValue");
161
+ });
162
+ });
163
+
164
+ describe("Variable Manager - Type Safety", () => {
165
+ test("should preserve type when getting typed variable", () => {
166
+ player.setVariable("count", 42);
167
+ const count = player.getVariable<number>("count");
168
+ expect(typeof count).toBe("number");
169
+ expect(count).toBe(42);
170
+ });
171
+
172
+ test("should work with complex nested objects", () => {
173
+ const complexData = {
174
+ player: {
175
+ stats: { hp: 100, mp: 50 },
176
+ inventory: [{ id: 1, name: "sword" }],
177
+ },
178
+ timestamp: Date.now(),
179
+ };
180
+ player.setVariable("gameState", complexData);
181
+ const retrieved = player.getVariable<typeof complexData>("gameState");
182
+ expect(retrieved).toEqual(complexData);
183
+ });
184
+ });
185
+