Files
personal-website/src/lib/game-tracker/rank-progress.test.ts
Stan Runge 0ee9ed0abb
All checks were successful
Test and deploy staging / staging (push) Successful in 2m29s
feat: roomier BO2 companion with +/- rank stepping
- Scale the COD page to the screen: larger gun art, text and progress
  ribbon, sized from one viewport unit so it fills a MacBook Pro 16"
  and big monitors and still fits 1280x720 without scrolling.
- Five columns on wide screens and four below, picked by CSS, so the
  server-rendered page already has the final layout.
- Replace the tabs/filters/overview rows with one toolbar of matching
  controls.
- Player and weapon ranks step with + and -: level 55 + 1 is the next
  prestige at level 1, up to max prestige. Presses show at once, batch
  while a save is running, and stepRank does the arithmetic in SQL.
- Clicking a rank still opens exact entry for big jumps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:50:19 +00:00

104 lines
4.7 KiB
TypeScript

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<string, number | null> = {
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
});
});
});