31 tests in test/, run in a plain Node env with a tiny localStorage shim (no jsdom — its Node-22.4 transitive-dep breakage isn't worth working around for logic tests): - type-chart: modern effectiveness, dual-type stacking, and the era rules (no Fairy pre-6, Steel resists Ghost/Dark pre-6, the Gen 1 Ghost→Psychic and Bug↔Poison quirks). - damage-calc: nature multipliers, the Gen 3+ stat formula (Garchomp reference values), STAB/effectiveness/immunity, pre/post-Gen-6 crit. - savedex: a synthetic Gen 1 SRAM (bitfields + checksum) round-trips; checksum-mismatch and size guards. - selection: normalizeSelection v2→v3 migration, per-game isolation, global favourite, stats vs statsNational. - pokedex-resolver: prettify, dex resolution against a mini snapshot. `npm test` wired into CI after format:check. Also: docker-compose host port 8080 → 4753 (README updated). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
84 lines
2.6 KiB
JavaScript
84 lines
2.6 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { natureMultiplier, statAt, calcDamage } from '../src/lib/damage-calc.js';
|
||
|
||
describe('natureMultiplier', () => {
|
||
it('boosts, cuts, and no-ops', () => {
|
||
expect(natureMultiplier('Adamant', 'atk')).toBe(1.1);
|
||
expect(natureMultiplier('Adamant', 'spa')).toBe(0.9);
|
||
expect(natureMultiplier('Adamant', 'spe')).toBe(1);
|
||
expect(natureMultiplier('Hardy', 'atk')).toBe(1); // neutral nature
|
||
expect(natureMultiplier('Nonsense', 'atk')).toBe(1); // unknown nature
|
||
});
|
||
});
|
||
|
||
describe('statAt (Gen 3+ formula)', () => {
|
||
it('HP and a normal stat at level 100, 31 IV / 0 EV / neutral', () => {
|
||
// Garchomp: 108 base HP -> 357 ; 130 base Atk -> 296
|
||
expect(statAt(108, 100, { statKey: 'hp' })).toBe(357);
|
||
expect(statAt(130, 100, { statKey: 'atk' })).toBe(296);
|
||
});
|
||
|
||
it('applies nature to the raw stat', () => {
|
||
expect(statAt(130, 100, { statKey: 'atk', nature: 'Adamant' })).toBe(325); // floor(296 * 1.1)
|
||
expect(statAt(130, 100, { statKey: 'atk', nature: 'Modest' })).toBe(266); // floor(296 * 0.9)
|
||
});
|
||
|
||
it('Shedinja HP is always 1', () => {
|
||
expect(statAt(1, 100, { statKey: 'hp' })).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('calcDamage', () => {
|
||
it('returns null for status / zero-power moves', () => {
|
||
expect(calcDamage({ power: 0, level: 50 })).toBeNull();
|
||
expect(calcDamage({ power: null, level: 50 })).toBeNull();
|
||
});
|
||
|
||
it('STAB + super effective, with the 85–100% roll', () => {
|
||
const r = calcDamage({
|
||
level: 100,
|
||
power: 80,
|
||
moveType: 'water',
|
||
atkTypes: ['water'],
|
||
defTypes: ['ground', 'rock'], // 4x
|
||
atkStat: 200,
|
||
defStat: 100,
|
||
gen: 9,
|
||
});
|
||
expect(r.stab).toBe(true);
|
||
expect(r.eff).toBe(4);
|
||
expect(r.max).toBeGreaterThan(r.min);
|
||
expect(r.min).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
it('immunity short-circuits to 0', () => {
|
||
const r = calcDamage({
|
||
level: 100,
|
||
power: 100,
|
||
moveType: 'ground',
|
||
atkTypes: ['fire'], // no STAB, to keep the shape unambiguous
|
||
defTypes: ['flying'],
|
||
atkStat: 200,
|
||
defStat: 100,
|
||
gen: 9,
|
||
});
|
||
expect(r).toEqual({ min: 0, max: 0, stab: false, eff: 0 });
|
||
});
|
||
|
||
it('crit multiplier differs pre/post Gen 6', () => {
|
||
const base = {
|
||
level: 100,
|
||
power: 100,
|
||
moveType: 'normal',
|
||
atkTypes: ['normal'],
|
||
defTypes: ['normal'],
|
||
atkStat: 200,
|
||
defStat: 100,
|
||
crit: true,
|
||
};
|
||
const g5 = calcDamage({ ...base, gen: 5 });
|
||
const g6 = calcDamage({ ...base, gen: 6 });
|
||
expect(g5.max).toBeGreaterThan(g6.max); // 2x vs 1.5x
|
||
});
|
||
});
|