import { describe, expect, it } from 'vitest'; import catalog from '../../../data/call-of-duty.json'; import type { MasteryRules, Weapon } from './mastery'; import { playerRankLimits, weaponRankLimits } from './rank-limits'; import { gameRanksProgress, hasReachedMaxLevel, rankProgress, stepRank, weaponFinished, weaponLevelsProgress } from './rank-progress'; const game = catalog.games.find((g) => g.slug === 'black-ops-2')!; const rules = { ...game, classes: game.classes.map((c) => c.name) } as MasteryRules; const m27: Weapon = { id: 1, name: 'M27', category: 'Assault Rifles', postLaunch: false }; const mp7: Weapon = { id: 2, name: 'MP7', category: 'Submachine Guns', postLaunch: false }; describe('BO2 rank progression', () => { it('counts every level in each of three weapon runs', () => { const limits = weaponRankLimits(rules, m27); expect(rankProgress(limits)).toEqual({ value: 0, total: 51 }); for (let prestige = 0; prestige <= 2; prestige++) { for (let level = 1; level <= 17; level++) { expect(rankProgress(limits, { level, prestige })).toEqual({ value: prestige * 17 + level, total: 51 }); } } }); it('weights the combined arsenal by levels and excludes unranked weapons', () => { const rpg = { ...m27, id: 3, name: 'RPG', category: 'Launchers' }; const ranks = new Map([ [1, { level: 9, prestige: 1 }], [2, { level: 8, prestige: 0 }] ]); expect(weaponLevelsProgress(rules, [m27, mp7, rpg], ranks)).toEqual({ value: 34, total: 99 }); expect(gameRanksProgress(rules, [m27, mp7, rpg], ranks, { level: 20, prestige: 2 })).toEqual({ value: 164, total: 704 }); }); it('uses class caps and individual exceptions', () => { const expected: Record = { M27: 17, MP7: 16, 'Vector K10': 16, 'Skorpion EVO': 16, Peacekeeper: 16, 'DSR 50': 10, Ballista: 11, Crossbow: 6, RPG: null, 'Combat Knife': null }; for (const c of game.classes) for (const name of [...c.weapons, ...('postLaunch' in c ? (c.postLaunch ?? []) : [])]) { const limits = weaponRankLimits(rules, { name, category: c.name }); if (name in expected) expect(limits.maxLevel, name).toBe(expected[name]); expect(limits.maxPrestige).toBe(2); } expect(playerRankLimits(rules).maxLevel).toBe(55); expect(rankProgress(playerRankLimits(rules), { level: 55, prestige: 10 })).toEqual({ value: 605, total: 605 }); }); it('keeps first-run attachment completion separate from all prestige runs', () => { expect(hasReachedMaxLevel(rules, m27, { level: 17, prestige: 0 })).toBe(true); expect(hasReachedMaxLevel(rules, m27, { level: 1, prestige: 1 })).toBe(true); expect(weaponFinished(rules, m27, { level: 17, prestige: 0 }, () => true)).toBe(false); expect(weaponFinished(rules, m27, { level: 16, prestige: 2 }, () => true)).toBe(false); expect(weaponFinished(rules, m27, { level: 17, prestige: 2 }, () => true)).toBe(true); }); it('steps level and prestige as one counter', () => { const player = playerRankLimits(rules); expect(stepRank(player, null, 1)).toEqual({ level: 2, prestige: 0 }); expect(stepRank(player, null, -1)).toEqual({ level: 1, prestige: 0 }); expect(stepRank(player, { level: 55, prestige: 0 }, 1)).toEqual({ level: 1, prestige: 1 }); expect(stepRank(player, { level: 1, prestige: 1 }, -1)).toEqual({ level: 55, prestige: 0 }); expect(stepRank(player, { level: 54, prestige: 10 }, 1)).toEqual({ level: 55, prestige: 10 }); expect(stepRank(player, { level: 55, prestige: 10 }, 1)).toEqual({ level: 55, prestige: 10 }); const m27Limits = weaponRankLimits(rules, m27); expect(stepRank(m27Limits, { level: 17, prestige: 1 }, 1)).toEqual({ level: 1, prestige: 2 }); expect(stepRank(m27Limits, { level: 16, prestige: 0 }, 3)).toEqual({ level: 2, prestige: 1 }); // Historic out-of-range prestiges count as max prestige instead of growing further. expect(stepRank(m27Limits, { level: 1, prestige: 4 }, -1)).toEqual({ level: 17, prestige: 1 }); expect(stepRank(m27Limits, { level: 1, prestige: 4 }, 1)).toEqual({ level: 2, prestige: 2 }); const uncapped = weaponRankLimits({ ...rules, rankCaps: undefined }, m27); expect(stepRank(uncapped, { level: 40, prestige: 3 }, 1)).toEqual({ level: 41, prestige: 3 }); expect(stepRank(uncapped, { level: 1, prestige: 3 }, -1)).toEqual({ level: 1, prestige: 3 }); }); it('bounds historic out-of-range values without mutating saved data', () => { const rank = { level: 100, prestige: 999 }; expect(rankProgress(weaponRankLimits(rules, m27), rank)).toEqual({ value: 51, total: 51 }); expect(rank).toEqual({ level: 100, prestige: 999 }); expect(rankProgress(weaponRankLimits({ ...rules, rankCaps: undefined }, m27), rank)).toEqual({ value: 0, total: 0 }); }); });