@rpgjs/server 5.0.0-alpha.4 → 5.0.0-alpha.41

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.
Files changed (101) hide show
  1. package/dist/Gui/DialogGui.d.ts +5 -0
  2. package/dist/Gui/GameoverGui.d.ts +23 -0
  3. package/dist/Gui/Gui.d.ts +6 -0
  4. package/dist/Gui/MenuGui.d.ts +22 -3
  5. package/dist/Gui/NotificationGui.d.ts +1 -2
  6. package/dist/Gui/SaveLoadGui.d.ts +13 -0
  7. package/dist/Gui/ShopGui.d.ts +28 -3
  8. package/dist/Gui/TitleGui.d.ts +23 -0
  9. package/dist/Gui/index.d.ts +10 -1
  10. package/dist/Player/BattleManager.d.ts +34 -12
  11. package/dist/Player/ClassManager.d.ts +46 -13
  12. package/dist/Player/ComponentManager.d.ts +123 -0
  13. package/dist/Player/Components.d.ts +345 -0
  14. package/dist/Player/EffectManager.d.ts +86 -0
  15. package/dist/Player/ElementManager.d.ts +104 -0
  16. package/dist/Player/GoldManager.d.ts +22 -0
  17. package/dist/Player/GuiManager.d.ts +259 -0
  18. package/dist/Player/ItemFixture.d.ts +6 -0
  19. package/dist/Player/ItemManager.d.ts +450 -9
  20. package/dist/Player/MoveManager.d.ts +324 -69
  21. package/dist/Player/ParameterManager.d.ts +344 -14
  22. package/dist/Player/Player.d.ts +460 -8
  23. package/dist/Player/SkillManager.d.ts +197 -15
  24. package/dist/Player/StateManager.d.ts +89 -25
  25. package/dist/Player/VariableManager.d.ts +74 -0
  26. package/dist/RpgServer.d.ts +502 -64
  27. package/dist/RpgServerEngine.d.ts +2 -1
  28. package/dist/decorators/event.d.ts +46 -0
  29. package/dist/decorators/map.d.ts +287 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +21653 -20900
  32. package/dist/index.js.map +1 -1
  33. package/dist/logs/log.d.ts +2 -3
  34. package/dist/module.d.ts +43 -1
  35. package/dist/presets/index.d.ts +0 -9
  36. package/dist/rooms/BaseRoom.d.ts +132 -0
  37. package/dist/rooms/lobby.d.ts +10 -2
  38. package/dist/rooms/map.d.ts +1236 -17
  39. package/dist/services/save.d.ts +43 -0
  40. package/dist/storage/index.d.ts +1 -0
  41. package/dist/storage/localStorage.d.ts +23 -0
  42. package/package.json +14 -10
  43. package/src/Gui/DialogGui.ts +19 -4
  44. package/src/Gui/GameoverGui.ts +39 -0
  45. package/src/Gui/Gui.ts +23 -1
  46. package/src/Gui/MenuGui.ts +155 -6
  47. package/src/Gui/NotificationGui.ts +1 -2
  48. package/src/Gui/SaveLoadGui.ts +60 -0
  49. package/src/Gui/ShopGui.ts +146 -16
  50. package/src/Gui/TitleGui.ts +39 -0
  51. package/src/Gui/index.ts +15 -2
  52. package/src/Player/BattleManager.ts +91 -49
  53. package/src/Player/ClassManager.ts +118 -50
  54. package/src/Player/ComponentManager.ts +425 -19
  55. package/src/Player/Components.ts +380 -0
  56. package/src/Player/EffectManager.ts +81 -44
  57. package/src/Player/ElementManager.ts +109 -86
  58. package/src/Player/GoldManager.ts +32 -35
  59. package/src/Player/GuiManager.ts +308 -150
  60. package/src/Player/ItemFixture.ts +4 -5
  61. package/src/Player/ItemManager.ts +774 -355
  62. package/src/Player/MoveManager.ts +1544 -774
  63. package/src/Player/ParameterManager.ts +546 -104
  64. package/src/Player/Player.ts +1163 -88
  65. package/src/Player/SkillManager.ts +520 -195
  66. package/src/Player/StateManager.ts +170 -182
  67. package/src/Player/VariableManager.ts +101 -63
  68. package/src/RpgServer.ts +525 -63
  69. package/src/core/context.ts +1 -0
  70. package/src/decorators/event.ts +61 -0
  71. package/src/decorators/map.ts +327 -0
  72. package/src/index.ts +11 -1
  73. package/src/logs/log.ts +10 -3
  74. package/src/module.ts +126 -3
  75. package/src/presets/index.ts +1 -10
  76. package/src/rooms/BaseRoom.ts +232 -0
  77. package/src/rooms/lobby.ts +25 -7
  78. package/src/rooms/map.ts +2502 -194
  79. package/src/services/save.ts +147 -0
  80. package/src/storage/index.ts +1 -0
  81. package/src/storage/localStorage.ts +76 -0
  82. package/tests/battle.spec.ts +375 -0
  83. package/tests/change-map.spec.ts +72 -0
  84. package/tests/class.spec.ts +274 -0
  85. package/tests/effect.spec.ts +219 -0
  86. package/tests/element.spec.ts +221 -0
  87. package/tests/event.spec.ts +80 -0
  88. package/tests/gold.spec.ts +99 -0
  89. package/tests/item.spec.ts +609 -0
  90. package/tests/module.spec.ts +38 -0
  91. package/tests/move.spec.ts +601 -0
  92. package/tests/player-param.spec.ts +28 -0
  93. package/tests/prediction-reconciliation.spec.ts +182 -0
  94. package/tests/random-move.spec.ts +65 -0
  95. package/tests/skill.spec.ts +658 -0
  96. package/tests/state.spec.ts +467 -0
  97. package/tests/variable.spec.ts +185 -0
  98. package/tests/world-maps.spec.ts +896 -0
  99. package/vite.config.ts +16 -0
  100. package/dist/Player/Event.d.ts +0 -0
  101. package/src/Player/Event.ts +0 -0
@@ -0,0 +1,147 @@
1
+ import { SaveSlot, SaveSlotEntries, SaveSlotList, SaveSlotMeta } from "@rpgjs/common";
2
+ import { inject } from "../core/inject";
3
+ import { RpgPlayer } from "../Player/Player";
4
+
5
+ export const SaveStorageToken = "SaveStorageToken";
6
+
7
+ export type SaveSlotIndex = number | "auto";
8
+
9
+ export interface SaveRequestContext {
10
+ reason?: "manual" | "auto" | "load";
11
+ source?: string;
12
+ }
13
+
14
+ export interface AutoSaveStrategy {
15
+ canSave?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
16
+ canLoad?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
17
+ shouldAutoSave?: (player: RpgPlayer, context?: SaveRequestContext) => boolean;
18
+ getDefaultSlot?: (player: RpgPlayer, context?: SaveRequestContext) => number | null;
19
+ }
20
+
21
+ export interface SaveStorageStrategy {
22
+ list(player: RpgPlayer): Promise<SaveSlotList>;
23
+ get(player: RpgPlayer, index: number): Promise<SaveSlot | null>;
24
+ save(player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void>;
25
+ delete?(player: RpgPlayer, index: number): Promise<void>;
26
+ }
27
+
28
+ type PlayerSlots = Map<string, SaveSlotEntries>;
29
+
30
+ export class InMemorySaveStorageStrategy implements SaveStorageStrategy {
31
+ private slotsByPlayer: PlayerSlots = new Map();
32
+
33
+ async list(player: RpgPlayer): Promise<SaveSlotList> {
34
+ return this.stripSnapshots(this.getSlots(player));
35
+ }
36
+
37
+ async get(player: RpgPlayer, index: number): Promise<SaveSlot | null> {
38
+ const slots = this.getSlots(player);
39
+ const slot = slots[index];
40
+ return slot ?? null;
41
+ }
42
+
43
+ async save(player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void> {
44
+ const slots = this.getSlots(player);
45
+ const existing = slots[index];
46
+ slots[index] = {
47
+ ...(existing ?? {}),
48
+ ...meta,
49
+ snapshot,
50
+ };
51
+ }
52
+
53
+ async delete(player: RpgPlayer, index: number): Promise<void> {
54
+ const slots = this.getSlots(player);
55
+ slots[index] = null;
56
+ }
57
+
58
+ private getSlots(player: RpgPlayer): SaveSlotEntries {
59
+ const key = player.id ?? "unknown";
60
+ if (!this.slotsByPlayer.has(key)) {
61
+ this.slotsByPlayer.set(key, []);
62
+ }
63
+ return this.slotsByPlayer.get(key)!;
64
+ }
65
+
66
+ private stripSnapshots(slots: SaveSlotEntries): SaveSlotList {
67
+ return slots.map((slot) => {
68
+ if (!slot) return null;
69
+ const { snapshot, ...meta } = slot;
70
+ return meta;
71
+ });
72
+ }
73
+ }
74
+
75
+ let cachedSaveStorage: SaveStorageStrategy | null = null;
76
+ let cachedAutoSave: AutoSaveStrategy | null = null;
77
+
78
+ export const AutoSaveToken = "AutoSaveToken";
79
+
80
+ export function resolveSaveStorageStrategy(): SaveStorageStrategy {
81
+ if (cachedSaveStorage) return cachedSaveStorage;
82
+ try {
83
+ cachedSaveStorage = inject<SaveStorageStrategy>(SaveStorageToken);
84
+ } catch {
85
+ cachedSaveStorage = new InMemorySaveStorageStrategy();
86
+ }
87
+ return cachedSaveStorage;
88
+ }
89
+
90
+ export function resolveAutoSaveStrategy(): AutoSaveStrategy {
91
+ if (cachedAutoSave) return cachedAutoSave;
92
+ try {
93
+ cachedAutoSave = inject<AutoSaveStrategy>(AutoSaveToken);
94
+ } catch {
95
+ cachedAutoSave = null;
96
+ }
97
+ cachedAutoSave ||= {
98
+ canSave: () => true,
99
+ canLoad: () => true,
100
+ shouldAutoSave: () => false,
101
+ getDefaultSlot: () => 0,
102
+ };
103
+ return cachedAutoSave;
104
+ }
105
+
106
+ export function resolveSaveSlot(
107
+ slot: SaveSlotIndex | undefined,
108
+ policy: AutoSaveStrategy,
109
+ player: RpgPlayer,
110
+ context?: SaveRequestContext
111
+ ): number | null {
112
+ if (typeof slot === "number") return slot;
113
+ const resolver = policy.getDefaultSlot;
114
+ if (!resolver) return null;
115
+ return resolver(player, context);
116
+ }
117
+
118
+ export function shouldAutoSave(player: RpgPlayer, context?: SaveRequestContext): boolean {
119
+ const strategy = resolveAutoSaveStrategy();
120
+ if (!strategy.shouldAutoSave) return false;
121
+ return strategy.shouldAutoSave(player, context);
122
+ }
123
+
124
+ export function buildSaveSlotMeta(player: RpgPlayer, overrides: SaveSlotMeta = {}): SaveSlotMeta {
125
+ const mapId = player.getCurrentMap()?.id;
126
+ const base: SaveSlotMeta = {
127
+ level: typeof player.level === "number" ? player.level : undefined,
128
+ exp: typeof player.exp === "number" ? player.exp : undefined,
129
+ map: typeof mapId === "string" ? mapId : undefined,
130
+ date: new Date().toISOString(),
131
+ };
132
+ return { ...base, ...overrides };
133
+ }
134
+
135
+ export function provideSaveStorage(strategy: SaveStorageStrategy) {
136
+ return {
137
+ provide: SaveStorageToken,
138
+ useValue: strategy,
139
+ };
140
+ }
141
+
142
+ export function provideAutoSave(strategy: AutoSaveStrategy) {
143
+ return {
144
+ provide: AutoSaveToken,
145
+ useValue: strategy,
146
+ };
147
+ }
@@ -0,0 +1 @@
1
+ export * from "./localStorage";
@@ -0,0 +1,76 @@
1
+ import type { SaveSlot, SaveSlotEntries, SaveSlotList, SaveSlotMeta } from "@rpgjs/common";
2
+ import type { SaveStorageStrategy } from "../services/save";
3
+ import { RpgPlayer } from "../Player/Player";
4
+
5
+ export interface LocalStorageSaveStorageOptions {
6
+ key?: string;
7
+ }
8
+
9
+ /**
10
+ * Save storage strategy backed by browser localStorage.
11
+ *
12
+ * Intended for standalone mode where the server runs in the browser
13
+ * and localStorage is available.
14
+ */
15
+ export class LocalStorageSaveStorageStrategy implements SaveStorageStrategy {
16
+ private key: string;
17
+
18
+ constructor(options: LocalStorageSaveStorageOptions = {}) {
19
+ this.key = options.key ?? "rpgjs-save-slots";
20
+ }
21
+
22
+ async list(_player: RpgPlayer): Promise<SaveSlotList> {
23
+ return this.stripSnapshots(this.readSlots());
24
+ }
25
+
26
+ async get(_player: RpgPlayer, index: number): Promise<SaveSlot | null> {
27
+ const slots = this.readSlots();
28
+ return slots[index] ?? null;
29
+ }
30
+
31
+ async save(_player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta): Promise<void> {
32
+ const slots = this.readSlots();
33
+ const existing = slots[index];
34
+ slots[index] = {
35
+ ...(existing ?? {}),
36
+ ...meta,
37
+ snapshot,
38
+ };
39
+ this.writeSlots(slots);
40
+ }
41
+
42
+ async delete(_player: RpgPlayer, index: number): Promise<void> {
43
+ const slots = this.readSlots();
44
+ slots[index] = null;
45
+ this.writeSlots(slots);
46
+ }
47
+
48
+ private readSlots(): SaveSlotEntries {
49
+ if (typeof localStorage === "undefined") {
50
+ return [];
51
+ }
52
+ const raw = localStorage.getItem(this.key);
53
+ if (!raw) return [];
54
+ try {
55
+ const parsed = JSON.parse(raw);
56
+ return Array.isArray(parsed) ? parsed : [];
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ private writeSlots(slots: SaveSlotEntries) {
63
+ if (typeof localStorage === "undefined") {
64
+ return;
65
+ }
66
+ localStorage.setItem(this.key, JSON.stringify(slots));
67
+ }
68
+
69
+ private stripSnapshots(slots: SaveSlotEntries): SaveSlotList {
70
+ return slots.map((slot) => {
71
+ if (!slot) return null;
72
+ const { snapshot, ...meta } = slot;
73
+ return meta;
74
+ });
75
+ }
76
+ }
@@ -0,0 +1,375 @@
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, MAXHP, MAXSP, ATK, PDEF, SDEF } from "../src";
5
+ import { Effect } from "../src/Player/EffectManager";
6
+
7
+ /**
8
+ * Test weapon for attack
9
+ */
10
+ const TestSword = {
11
+ id: "test-sword",
12
+ name: "Test Sword",
13
+ atk: 50,
14
+ _type: "weapon" as const,
15
+ };
16
+
17
+ /**
18
+ * Test armor for defense
19
+ */
20
+ const TestArmor = {
21
+ id: "test-armor",
22
+ name: "Test Armor",
23
+ pdef: 30,
24
+ sdef: 20,
25
+ _type: "armor" as const,
26
+ };
27
+
28
+ /**
29
+ * Test skill for magical damage
30
+ */
31
+ const FireSkill = {
32
+ id: "fire-skill",
33
+ name: "Fire",
34
+ spCost: 10,
35
+ hitRate: 1,
36
+ power: 50,
37
+ coefficient: { [ATK]: 1, [PDEF]: 1 },
38
+ _type: "skill" as const,
39
+ };
40
+
41
+ /**
42
+ * Damage formulas for testing
43
+ */
44
+ const damageFormulas = {
45
+ // Physical damage: ATK - PDEF/2
46
+ damagePhysic: (a: any, b: any) => Math.max(0, a[ATK] - b[PDEF] / 2),
47
+
48
+ // Skill damage: power + ATK coefficient - PDEF coefficient
49
+ damageSkill: (a: any, b: any, skill: any) => {
50
+ const power = skill.power + (a[ATK] * (skill.coefficient?.[ATK] || 0));
51
+ return Math.max(0, power - (b[PDEF] * (skill.coefficient?.[PDEF] || 0)) / 2);
52
+ },
53
+
54
+ // Critical damage: 1.5x with 10% chance
55
+ damageCritical: (damage: number, a: any, b: any) => {
56
+ if (Math.random() < 0.1) {
57
+ return damage * 1.5;
58
+ }
59
+ return damage;
60
+ },
61
+
62
+ // Guard: reduce damage by 50%
63
+ damageGuard: (damage: number, a: any, b: any) => damage * 0.5,
64
+
65
+ // Element coefficient formula
66
+ coefficientElements: (atkElement: any, defElement: any, defElementDef: any) => {
67
+ return (atkElement.rate * defElement.rate) - defElementDef.rate;
68
+ },
69
+ };
70
+
71
+ let player: RpgPlayer;
72
+ let attackerPlayer: RpgPlayer;
73
+ let fixture: TestingFixture;
74
+
75
+ const serverModule = defineModule({
76
+ maps: [
77
+ {
78
+ id: "test-map",
79
+ file: "",
80
+ },
81
+ ],
82
+ database: {
83
+ "test-sword": TestSword,
84
+ "test-armor": TestArmor,
85
+ "fire-skill": FireSkill,
86
+ },
87
+ player: {
88
+ async onConnected(player) {
89
+ await player.changeMap("test-map", { x: 100, y: 100 });
90
+ },
91
+ },
92
+ });
93
+
94
+ const clientModule = defineModule({});
95
+
96
+ beforeEach(async () => {
97
+ const myModule = createModule("TestModule", [
98
+ { server: serverModule, client: clientModule },
99
+ ]);
100
+ fixture = await testing(myModule);
101
+
102
+ // Create defender player
103
+ const clientTesting = await fixture.createClient();
104
+ player = await clientTesting.waitForMapChange("test-map");
105
+ player.hp = 1000;
106
+ player.param[MAXHP] = 1000;
107
+ player.param[PDEF] = 20;
108
+ player.param[SDEF] = 10;
109
+
110
+ // Create attacker player
111
+ const clientTesting2 = await fixture.createClient();
112
+ attackerPlayer = await clientTesting2.waitForMapChange("test-map");
113
+ attackerPlayer.hp = 1000;
114
+ attackerPlayer.param[MAXHP] = 1000;
115
+ attackerPlayer.param[ATK] = 50;
116
+
117
+ // Set damage formulas on map
118
+ const map = player.getCurrentMap();
119
+ if (map) {
120
+ (map as any).damageFormulas = damageFormulas;
121
+ }
122
+ });
123
+
124
+ afterEach(async () => {
125
+ await fixture.clear();
126
+ });
127
+
128
+ describe("Battle Manager - applyDamage (Physical)", () => {
129
+ test("should apply physical damage", () => {
130
+ const initialHp = player.hp;
131
+ const result = player.applyDamage(attackerPlayer);
132
+
133
+ expect(result).toBeDefined();
134
+ expect(result.damage).toBeGreaterThanOrEqual(0);
135
+ });
136
+
137
+ test("should return damage result object", () => {
138
+ const result = player.applyDamage(attackerPlayer);
139
+
140
+ expect(result).toHaveProperty("damage");
141
+ expect(result).toHaveProperty("critical");
142
+ expect(result).toHaveProperty("elementVulnerable");
143
+ expect(result).toHaveProperty("guard");
144
+ expect(result).toHaveProperty("superGuard");
145
+ });
146
+
147
+ test("should reduce HP by damage amount", () => {
148
+ const initialHp = player.hp;
149
+ const result = player.applyDamage(attackerPlayer);
150
+
151
+ expect(player.hp).toBe(initialHp - result.damage);
152
+ });
153
+
154
+ // Note: Damage calculation depends on formula configuration on map
155
+ // which may not be fully accessible in test environment
156
+ test.skip("should calculate damage based on ATK and PDEF", () => {
157
+ // ATK 50, PDEF 20 -> damage = 50 - 20/2 = 40
158
+ attackerPlayer.param[ATK] = 50;
159
+ player.param[PDEF] = 20;
160
+
161
+ // Disable critical for predictable test
162
+ const originalRandom = Math.random;
163
+ Math.random = vi.fn(() => 0.5); // Won't trigger critical
164
+
165
+ const result = player.applyDamage(attackerPlayer);
166
+ expect(result.damage).toBe(40);
167
+
168
+ Math.random = originalRandom;
169
+ });
170
+ });
171
+
172
+ describe("Battle Manager - applyDamage (Skill)", () => {
173
+ test("should apply skill damage", () => {
174
+ player.learnSkill("fire-skill");
175
+ const initialHp = player.hp;
176
+
177
+ const result = player.applyDamage(attackerPlayer, FireSkill);
178
+
179
+ expect(result).toBeDefined();
180
+ expect(result.damage).toBeGreaterThanOrEqual(0);
181
+ expect(player.hp).toBeLessThan(initialHp);
182
+ });
183
+
184
+ test("should calculate skill damage based on skill properties", () => {
185
+ // power 50 + ATK * coefficient - PDEF * coefficient / 2
186
+ const result = player.applyDamage(attackerPlayer, FireSkill);
187
+ expect(result.damage).toBeGreaterThanOrEqual(0);
188
+ });
189
+
190
+ test("should throw error if skill formulas not defined", () => {
191
+ const map = player.getCurrentMap();
192
+ if (map) {
193
+ const oldFormulas = (map as any).damageFormulas;
194
+ (map as any).damageFormulas = {};
195
+
196
+ expect(() => {
197
+ player.applyDamage(attackerPlayer, FireSkill);
198
+ }).toThrow("Skill Formulas not exists");
199
+
200
+ (map as any).damageFormulas = oldFormulas;
201
+ }
202
+ });
203
+ });
204
+
205
+ describe("Battle Manager - Critical Hits", () => {
206
+ // Note: Critical detection depends on damageCritical formula which
207
+ // requires map formula configuration
208
+ test.skip("should detect critical hit", () => {
209
+ const originalRandom = Math.random;
210
+ Math.random = vi.fn(() => 0.05); // 5% < 10% threshold
211
+
212
+ const result = player.applyDamage(attackerPlayer);
213
+ expect(result.critical).toBe(true);
214
+
215
+ Math.random = originalRandom;
216
+ });
217
+
218
+ test("should not critical when formula not defined", () => {
219
+ const originalRandom = Math.random;
220
+ Math.random = vi.fn(() => 0.5); // 50% > 10% threshold
221
+
222
+ const result = player.applyDamage(attackerPlayer);
223
+ expect(result.critical).toBe(false);
224
+
225
+ Math.random = originalRandom;
226
+ });
227
+
228
+ test.skip("should increase damage on critical", () => {
229
+ const originalRandom = Math.random;
230
+
231
+ // Non-critical damage
232
+ Math.random = vi.fn(() => 0.5);
233
+ const normalResult = player.applyDamage(attackerPlayer);
234
+ player.hp = 1000; // Reset HP
235
+
236
+ // Critical damage
237
+ Math.random = vi.fn(() => 0.05);
238
+ const criticalResult = player.applyDamage(attackerPlayer);
239
+
240
+ if (criticalResult.critical) {
241
+ expect(criticalResult.damage).toBeGreaterThan(normalResult.damage);
242
+ }
243
+
244
+ Math.random = originalRandom;
245
+ });
246
+ });
247
+
248
+ describe("Battle Manager - Guard Effect", () => {
249
+ // Note: Guard detection depends on damageGuard formula which
250
+ // requires map formula configuration
251
+ test.skip("should detect guard effect", () => {
252
+ player.effects = [Effect.GUARD];
253
+
254
+ const result = player.applyDamage(attackerPlayer);
255
+ expect(result.guard).toBe(true);
256
+ });
257
+
258
+ test.skip("should reduce damage with guard", () => {
259
+ const originalRandom = Math.random;
260
+ Math.random = vi.fn(() => 0.5); // No critical
261
+
262
+ // Normal damage
263
+ const normalResult = player.applyDamage(attackerPlayer);
264
+ player.hp = 1000; // Reset HP
265
+
266
+ // Guard damage
267
+ player.effects = [Effect.GUARD];
268
+ const guardResult = player.applyDamage(attackerPlayer);
269
+
270
+ expect(guardResult.guard).toBe(true);
271
+ expect(guardResult.damage).toBeLessThan(normalResult.damage);
272
+
273
+ Math.random = originalRandom;
274
+ });
275
+
276
+ test("should have guard effect active when set", () => {
277
+ player.effects = [Effect.GUARD];
278
+ expect(player.hasEffect(Effect.GUARD)).toBe(true);
279
+ });
280
+ });
281
+
282
+ describe("Battle Manager - Super Guard Effect", () => {
283
+ test("should detect super guard effect", () => {
284
+ player.effects = [Effect.SUPER_GUARD];
285
+
286
+ const result = player.applyDamage(attackerPlayer);
287
+ expect(result.superGuard).toBe(true);
288
+ });
289
+
290
+ test("should reduce damage by 75% with super guard", () => {
291
+ const originalRandom = Math.random;
292
+ Math.random = vi.fn(() => 0.5); // No critical
293
+
294
+ // Normal damage
295
+ const normalResult = player.applyDamage(attackerPlayer);
296
+ player.hp = 1000; // Reset HP
297
+
298
+ // Super guard damage (1/4 of normal)
299
+ player.effects = [Effect.SUPER_GUARD];
300
+ const superGuardResult = player.applyDamage(attackerPlayer);
301
+
302
+ expect(superGuardResult.superGuard).toBe(true);
303
+ expect(superGuardResult.damage).toBe(normalResult.damage / 4);
304
+
305
+ Math.random = originalRandom;
306
+ });
307
+ });
308
+
309
+ describe("Battle Manager - Element Vulnerability", () => {
310
+ test("should not have element vulnerability by default", () => {
311
+ const originalRandom = Math.random;
312
+ Math.random = vi.fn(() => 0.5);
313
+
314
+ const result = player.applyDamage(attackerPlayer);
315
+ // Without elements, should not be vulnerable
316
+ expect(result.elementVulnerable).toBe(false);
317
+
318
+ Math.random = originalRandom;
319
+ });
320
+ });
321
+
322
+ describe("Battle Manager - getFormulas", () => {
323
+ test("should get damage formulas from map", () => {
324
+ const formula = (player as any).getFormulas("damagePhysic");
325
+ expect(formula).toBeDefined();
326
+ expect(typeof formula).toBe("function");
327
+ });
328
+
329
+ test("should get skill damage formula", () => {
330
+ const formula = (player as any).getFormulas("damageSkill");
331
+ expect(formula).toBeDefined();
332
+ });
333
+
334
+ test("should return undefined for non-existent formula", () => {
335
+ const formula = (player as any).getFormulas("nonExistent");
336
+ expect(formula).toBeUndefined();
337
+ });
338
+ });
339
+
340
+ describe("Battle Manager - Edge Cases", () => {
341
+ test("should handle 0 ATK attacker", () => {
342
+ attackerPlayer.param[ATK] = 0;
343
+
344
+ const result = player.applyDamage(attackerPlayer);
345
+ expect(result.damage).toBe(0);
346
+ });
347
+
348
+ test("should not deal negative damage", () => {
349
+ attackerPlayer.param[ATK] = 10;
350
+ player.param[PDEF] = 100;
351
+
352
+ const result = player.applyDamage(attackerPlayer);
353
+ expect(result.damage).toBeGreaterThanOrEqual(0);
354
+ });
355
+
356
+ test("should handle multiple consecutive attacks", () => {
357
+ player.hp = 1000;
358
+
359
+ const originalRandom = Math.random;
360
+ Math.random = vi.fn(() => 0.5);
361
+
362
+ const results: any[] = [];
363
+ for (let i = 0; i < 5; i++) {
364
+ results.push(player.applyDamage(attackerPlayer));
365
+ }
366
+
367
+ // Each attack should deal damage
368
+ results.forEach(result => {
369
+ expect(result.damage).toBeGreaterThanOrEqual(0);
370
+ });
371
+
372
+ Math.random = originalRandom;
373
+ });
374
+ });
375
+
@@ -0,0 +1,72 @@
1
+ import { beforeEach, test, expect, afterEach } 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 server module with two maps
8
+ const serverModule = defineModule<RpgServer>({
9
+ maps: [
10
+ {
11
+ id: 'map1',
12
+ file: '',
13
+ },
14
+ {
15
+ id: 'map2',
16
+ file: '',
17
+ }
18
+ ],
19
+ player: {
20
+ async onConnected(player) {
21
+ // Start player on map1
22
+ await player.changeMap('map1', { x: 100, y: 100 })
23
+ },
24
+ onJoinMap(player) {
25
+ console.log('onJoinMap', player.getCurrentMap()?.id)
26
+ }
27
+ }
28
+ })
29
+
30
+ // Define client module
31
+ const clientModule = defineModule<RpgClient>({
32
+ // Client-side logic
33
+ })
34
+
35
+ let player: RpgPlayer
36
+ let client: any
37
+ let fixture: any
38
+
39
+ beforeEach(async () => {
40
+ const myModule = createModule('TestModule', [{
41
+ server: serverModule,
42
+ client: clientModule
43
+ }])
44
+
45
+ fixture = await testing(myModule)
46
+ client = await fixture.createClient()
47
+ player = client.player
48
+ })
49
+
50
+ afterEach(() => {
51
+ fixture.clear()
52
+ })
53
+
54
+ test('Player can change map', async () => {
55
+ player = await client.waitForMapChange('map1')
56
+
57
+ const initialMap = player.getCurrentMap()
58
+ expect(initialMap).toBeDefined()
59
+ expect(initialMap?.id).toBe('map1')
60
+
61
+ const result = await player.changeMap('map2', { x: 200, y: 200 })
62
+ expect(result).toBe(true)
63
+
64
+ player = await client.waitForMapChange('map2')
65
+
66
+ const newMap = player.getCurrentMap()
67
+ expect(newMap).toBeDefined()
68
+ expect(newMap?.id).toBe('map2')
69
+
70
+ expect(player.x()).toBe(200)
71
+ expect(player.y()).toBe(200)
72
+ })