|
| 1 | +import { describe, it, expect, beforeEach, vi } from 'vitest'; |
| 2 | +import RebirthManager from '../systems/RebirthManager.js'; |
| 3 | + |
| 4 | +// Mock localStorage |
| 5 | +const store = {}; |
| 6 | +const localStorageMock = { |
| 7 | + getItem: vi.fn(key => store[key] ?? null), |
| 8 | + setItem: vi.fn((key, val) => { store[key] = val; }), |
| 9 | + removeItem: vi.fn(key => { delete store[key]; }), |
| 10 | + clear: vi.fn(() => { Object.keys(store).forEach(k => delete store[k]); }) |
| 11 | +}; |
| 12 | +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock }); |
| 13 | + |
| 14 | +describe('RebirthManager', () => { |
| 15 | + beforeEach(() => { |
| 16 | + localStorageMock.clear(); |
| 17 | + vi.clearAllMocks(); |
| 18 | + }); |
| 19 | + |
| 20 | + describe('MILESTONES', () => { |
| 21 | + it('has 5 milestones', () => { |
| 22 | + expect(RebirthManager.MILESTONES).toHaveLength(5); |
| 23 | + }); |
| 24 | + |
| 25 | + it('milestones are in ascending wave order', () => { |
| 26 | + for (let i = 1; i < RebirthManager.MILESTONES.length; i++) { |
| 27 | + expect(RebirthManager.MILESTONES[i].wave).toBeGreaterThan( |
| 28 | + RebirthManager.MILESTONES[i - 1].wave |
| 29 | + ); |
| 30 | + } |
| 31 | + }); |
| 32 | + |
| 33 | + it('each milestone has required fields', () => { |
| 34 | + RebirthManager.MILESTONES.forEach(m => { |
| 35 | + expect(m).toHaveProperty('wave'); |
| 36 | + expect(m).toHaveProperty('rebirth'); |
| 37 | + expect(m).toHaveProperty('name'); |
| 38 | + expect(typeof m.wave).toBe('number'); |
| 39 | + expect(typeof m.rebirth).toBe('number'); |
| 40 | + expect(typeof m.name).toBe('string'); |
| 41 | + }); |
| 42 | + }); |
| 43 | + |
| 44 | + it('rebirth levels are sequential 1-5', () => { |
| 45 | + const levels = RebirthManager.MILESTONES.map(m => m.rebirth); |
| 46 | + expect(levels).toEqual([1, 2, 3, 4, 5]); |
| 47 | + }); |
| 48 | + }); |
| 49 | + |
| 50 | + describe('load', () => { |
| 51 | + it('returns default state when nothing saved', () => { |
| 52 | + const data = RebirthManager.load(); |
| 53 | + expect(data).toEqual({ |
| 54 | + rebirthLevel: 0, |
| 55 | + highestWave: 0, |
| 56 | + totalRebirths: 0, |
| 57 | + lifetimeKills: 0 |
| 58 | + }); |
| 59 | + }); |
| 60 | + |
| 61 | + it('loads saved data from localStorage', () => { |
| 62 | + const saved = { rebirthLevel: 3, highestWave: 160, totalRebirths: 5, lifetimeKills: 999 }; |
| 63 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify(saved); |
| 64 | + |
| 65 | + const data = RebirthManager.load(); |
| 66 | + expect(data).toEqual(saved); |
| 67 | + }); |
| 68 | + |
| 69 | + it('returns defaults on corrupted JSON', () => { |
| 70 | + store[RebirthManager.STORAGE_KEY] = '{broken'; |
| 71 | + const data = RebirthManager.load(); |
| 72 | + expect(data.rebirthLevel).toBe(0); |
| 73 | + }); |
| 74 | + }); |
| 75 | + |
| 76 | + describe('save', () => { |
| 77 | + it('persists data to localStorage', () => { |
| 78 | + const data = { rebirthLevel: 2, highestWave: 110, totalRebirths: 3, lifetimeKills: 500 }; |
| 79 | + RebirthManager.save(data); |
| 80 | + expect(localStorageMock.setItem).toHaveBeenCalledWith( |
| 81 | + RebirthManager.STORAGE_KEY, |
| 82 | + JSON.stringify(data) |
| 83 | + ); |
| 84 | + }); |
| 85 | + }); |
| 86 | + |
| 87 | + describe('canRebirth', () => { |
| 88 | + it('returns first milestone when no rebirths and wave >= 50', () => { |
| 89 | + const milestone = RebirthManager.canRebirth(50); |
| 90 | + expect(milestone).not.toBeNull(); |
| 91 | + expect(milestone.rebirth).toBe(1); |
| 92 | + expect(milestone.name).toBe('JUNIOR DEV'); |
| 93 | + }); |
| 94 | + |
| 95 | + it('returns null when wave is too low', () => { |
| 96 | + expect(RebirthManager.canRebirth(10)).toBeNull(); |
| 97 | + expect(RebirthManager.canRebirth(49)).toBeNull(); |
| 98 | + }); |
| 99 | + |
| 100 | + it('returns highest available milestone for high waves', () => { |
| 101 | + const milestone = RebirthManager.canRebirth(250); |
| 102 | + expect(milestone.rebirth).toBe(5); |
| 103 | + expect(milestone.name).toBe('ARCHITECT'); |
| 104 | + }); |
| 105 | + |
| 106 | + it('returns null when already at max rebirth level', () => { |
| 107 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 108 | + rebirthLevel: 5, highestWave: 300, totalRebirths: 10, lifetimeKills: 5000 |
| 109 | + }); |
| 110 | + expect(RebirthManager.canRebirth(300)).toBeNull(); |
| 111 | + }); |
| 112 | + |
| 113 | + it('returns next available milestone after current rebirth', () => { |
| 114 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 115 | + rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 200 |
| 116 | + }); |
| 117 | + const milestone = RebirthManager.canRebirth(150); |
| 118 | + expect(milestone.rebirth).toBe(3); |
| 119 | + expect(milestone.name).toBe('SENIOR DEV'); |
| 120 | + }); |
| 121 | + }); |
| 122 | + |
| 123 | + describe('performRebirth', () => { |
| 124 | + it('updates rebirth level to milestone', () => { |
| 125 | + const result = RebirthManager.performRebirth(50, 100); |
| 126 | + expect(result.rebirthLevel).toBe(1); |
| 127 | + expect(result.totalRebirths).toBe(1); |
| 128 | + expect(result.lifetimeKills).toBe(100); |
| 129 | + }); |
| 130 | + |
| 131 | + it('tracks highest wave', () => { |
| 132 | + const result = RebirthManager.performRebirth(75, 50); |
| 133 | + expect(result.highestWave).toBe(75); |
| 134 | + }); |
| 135 | + |
| 136 | + it('does not lower highest wave on subsequent rebirth', () => { |
| 137 | + // First rebirth at wave 75 |
| 138 | + RebirthManager.performRebirth(75, 50); |
| 139 | + // Second rebirth at wave 100 (lower than stored highest could hypothetically be higher) |
| 140 | + const result = RebirthManager.performRebirth(100, 80); |
| 141 | + expect(result.highestWave).toBe(100); |
| 142 | + }); |
| 143 | + |
| 144 | + it('returns unchanged data when no milestone available', () => { |
| 145 | + const result = RebirthManager.performRebirth(10, 5); |
| 146 | + expect(result.rebirthLevel).toBe(0); |
| 147 | + expect(result.totalRebirths).toBe(0); |
| 148 | + }); |
| 149 | + |
| 150 | + it('accumulates lifetime kills', () => { |
| 151 | + RebirthManager.performRebirth(50, 100); |
| 152 | + const result = RebirthManager.performRebirth(100, 200); |
| 153 | + expect(result.lifetimeKills).toBe(300); |
| 154 | + }); |
| 155 | + }); |
| 156 | + |
| 157 | + describe('getAllStatsMultiplier', () => { |
| 158 | + it('returns 1.0 with no rebirths', () => { |
| 159 | + expect(RebirthManager.getAllStatsMultiplier()).toBe(1); |
| 160 | + }); |
| 161 | + |
| 162 | + it('returns 1.05 at rebirth level 1', () => { |
| 163 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 164 | + rebirthLevel: 1, highestWave: 50, totalRebirths: 1, lifetimeKills: 0 |
| 165 | + }); |
| 166 | + expect(RebirthManager.getAllStatsMultiplier()).toBeCloseTo(1.05); |
| 167 | + }); |
| 168 | + |
| 169 | + it('returns 1.25 at rebirth level 5', () => { |
| 170 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 171 | + rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0 |
| 172 | + }); |
| 173 | + expect(RebirthManager.getAllStatsMultiplier()).toBeCloseTo(1.25); |
| 174 | + }); |
| 175 | + }); |
| 176 | + |
| 177 | + describe('getXPMultiplier', () => { |
| 178 | + it('returns 1.0 with no rebirths', () => { |
| 179 | + expect(RebirthManager.getXPMultiplier()).toBe(1); |
| 180 | + }); |
| 181 | + |
| 182 | + it('scales by 10% per rebirth level', () => { |
| 183 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 184 | + rebirthLevel: 3, highestWave: 150, totalRebirths: 3, lifetimeKills: 0 |
| 185 | + }); |
| 186 | + expect(RebirthManager.getXPMultiplier()).toBeCloseTo(1.3); |
| 187 | + }); |
| 188 | + }); |
| 189 | + |
| 190 | + describe('getStartingWeaponCount', () => { |
| 191 | + it('returns 0 with no rebirths', () => { |
| 192 | + expect(RebirthManager.getStartingWeaponCount()).toBe(0); |
| 193 | + }); |
| 194 | + |
| 195 | + it('returns rebirth level up to cap of 3', () => { |
| 196 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 197 | + rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0 |
| 198 | + }); |
| 199 | + expect(RebirthManager.getStartingWeaponCount()).toBe(2); |
| 200 | + }); |
| 201 | + |
| 202 | + it('caps at 3 for high rebirth levels', () => { |
| 203 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 204 | + rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0 |
| 205 | + }); |
| 206 | + expect(RebirthManager.getStartingWeaponCount()).toBe(3); |
| 207 | + }); |
| 208 | + }); |
| 209 | + |
| 210 | + describe('getStartingWeapons', () => { |
| 211 | + it('returns empty array with no rebirths', () => { |
| 212 | + expect(RebirthManager.getStartingWeapons()).toEqual([]); |
| 213 | + }); |
| 214 | + |
| 215 | + it('returns correct number of weapons', () => { |
| 216 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 217 | + rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0 |
| 218 | + }); |
| 219 | + const weapons = RebirthManager.getStartingWeapons(); |
| 220 | + expect(weapons).toHaveLength(2); |
| 221 | + }); |
| 222 | + |
| 223 | + it('returns weapons from the valid pool', () => { |
| 224 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 225 | + rebirthLevel: 3, highestWave: 150, totalRebirths: 3, lifetimeKills: 0 |
| 226 | + }); |
| 227 | + const validPool = ['spread', 'pierce', 'rapid', 'homing', 'bounce', 'aoe', 'freeze']; |
| 228 | + const weapons = RebirthManager.getStartingWeapons(); |
| 229 | + weapons.forEach(w => { |
| 230 | + expect(validPool).toContain(w); |
| 231 | + }); |
| 232 | + }); |
| 233 | + |
| 234 | + it('returns no duplicate weapons', () => { |
| 235 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 236 | + rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0 |
| 237 | + }); |
| 238 | + const weapons = RebirthManager.getStartingWeapons(); |
| 239 | + expect(new Set(weapons).size).toBe(weapons.length); |
| 240 | + }); |
| 241 | + }); |
| 242 | + |
| 243 | + describe('getRebirthInfo', () => { |
| 244 | + it('returns INTERN name at level 0', () => { |
| 245 | + const info = RebirthManager.getRebirthInfo(); |
| 246 | + expect(info.name).toBe('INTERN'); |
| 247 | + expect(info.level).toBe(0); |
| 248 | + }); |
| 249 | + |
| 250 | + it('returns correct milestone name', () => { |
| 251 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 252 | + rebirthLevel: 3, highestWave: 160, totalRebirths: 4, lifetimeKills: 800 |
| 253 | + }); |
| 254 | + const info = RebirthManager.getRebirthInfo(); |
| 255 | + expect(info.name).toBe('SENIOR DEV'); |
| 256 | + expect(info.level).toBe(3); |
| 257 | + }); |
| 258 | + |
| 259 | + it('calculates bonus percentages correctly', () => { |
| 260 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 261 | + rebirthLevel: 2, highestWave: 110, totalRebirths: 3, lifetimeKills: 500 |
| 262 | + }); |
| 263 | + const info = RebirthManager.getRebirthInfo(); |
| 264 | + expect(info.allStatsBonus).toBe(10); // 2 * 5% |
| 265 | + expect(info.xpBonus).toBe(20); // 2 * 10% |
| 266 | + }); |
| 267 | + |
| 268 | + it('includes next milestone info', () => { |
| 269 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 270 | + rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0 |
| 271 | + }); |
| 272 | + const info = RebirthManager.getRebirthInfo(); |
| 273 | + expect(info.nextMilestone).toBeDefined(); |
| 274 | + expect(info.nextMilestone.rebirth).toBe(3); |
| 275 | + }); |
| 276 | + |
| 277 | + it('has no next milestone at max level', () => { |
| 278 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 279 | + rebirthLevel: 5, highestWave: 300, totalRebirths: 10, lifetimeKills: 5000 |
| 280 | + }); |
| 281 | + const info = RebirthManager.getRebirthInfo(); |
| 282 | + expect(info.nextMilestone).toBeUndefined(); |
| 283 | + }); |
| 284 | + |
| 285 | + it('includes lifetime stats', () => { |
| 286 | + store[RebirthManager.STORAGE_KEY] = JSON.stringify({ |
| 287 | + rebirthLevel: 1, highestWave: 55, totalRebirths: 2, lifetimeKills: 300 |
| 288 | + }); |
| 289 | + const info = RebirthManager.getRebirthInfo(); |
| 290 | + expect(info.totalRebirths).toBe(2); |
| 291 | + expect(info.lifetimeKills).toBe(300); |
| 292 | + expect(info.highestWave).toBe(55); |
| 293 | + }); |
| 294 | + }); |
| 295 | +}); |
0 commit comments