diff --git a/src/PersonObjects/formulas/skill.ts b/src/PersonObjects/formulas/skill.ts index 5d9c8a895..405e90254 100644 --- a/src/PersonObjects/formulas/skill.ts +++ b/src/PersonObjects/formulas/skill.ts @@ -10,7 +10,21 @@ export function calculateSkill(exp: number, mult = 1): number { } export function calculateExp(skill: number, mult = 1): number { - const value = Math.exp((skill / mult + 200) / 32) - 534.6; + const floorSkill = Math.floor(skill); + let value = Math.exp((skill / mult + 200) / 32) - 534.6; + if (skill === floorSkill && Number.isFinite(skill)) { + // Check for floating point rounding issues that would cause the inverse + // operation to return the wrong result. + let calcSkill = calculateSkill(value, mult); + let diff = Math.abs(value * Number.EPSILON); + let newValue = value; + while (calcSkill < skill) { + newValue = value + diff; + diff *= 2; + calcSkill = calculateSkill(newValue, mult); + } + value = newValue; + } return clampNumber(value, 0); } diff --git a/test/jest/formulas/Skill.test.ts b/test/jest/formulas/Skill.test.ts new file mode 100644 index 000000000..3e2870242 --- /dev/null +++ b/test/jest/formulas/Skill.test.ts @@ -0,0 +1,34 @@ +import { calculateSkill, calculateExp } from "../../../src/PersonObjects/formulas/skill"; + +describe("calculateSkill", () => { + test.each([...new Array(300).keys()])("Correct inverse %i", (skill: number) => { + if (skill < 2) return; // There are special cases to be dealt with + const xp1 = calculateExp(skill); + expect(calculateSkill(xp1)).toBe(skill); + expect(calculateSkill(xp1 * 0.999999999)).toBe(skill - 1); + + const xp2 = calculateExp(skill, 1.4); + expect(calculateSkill(xp2, 1.4)).toBe(skill); + expect(calculateSkill(xp2 * 0.999999999, 1.4)).toBe(skill - 1); + + if (skill < 4) return; // 3 is a special case for this mult + const xp3 = calculateExp(skill, 3.3); + expect(calculateSkill(xp3, 3.3)).toBe(skill); + expect(calculateSkill(xp3 * 0.999999999, 3.3)).toBe(skill - 1); + expect(calculateSkill(calculateExp(skill, 3.3), 3.3)).toBe(skill); + }); + test("Special cases", () => { + expect(() => calculateExp(NaN)).toThrow(); + expect(calculateExp(Infinity)).toBe(Number.MAX_VALUE); + expect(calculateExp(-Infinity)).toBe(0); + + // In all these cases, the XP is clamped to 0. With a big enough mult, + // this gets converted back to a larger skill. + expect(calculateSkill(calculateExp(0))).toBe(1); + expect(calculateSkill(calculateExp(0, 1.4), 1.4)).toBe(1); + expect(calculateSkill(calculateExp(0, 3.3), 3.3)).toBe(3); + expect(calculateSkill(calculateExp(1))).toBe(1); + expect(calculateSkill(calculateExp(1, 1.4), 1.4)).toBe(1); + expect(calculateSkill(calculateExp(1, 3.3), 3.3)).toBe(3); + }); +});