From 666b61ec09aed73efb01c4ceb79b76b07ca05a81 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Sun, 13 Feb 2022 18:35:15 +0100 Subject: [PATCH] test: add tests for calculating the spell price --- spec/item/type-specific-helpers/spell.spec.ts | 231 ++++++++++++++++++ spec/setup.ts | 30 +++ 2 files changed, 261 insertions(+) create mode 100644 spec/item/type-specific-helpers/spell.spec.ts diff --git a/spec/item/type-specific-helpers/spell.spec.ts b/spec/item/type-specific-helpers/spell.spec.ts new file mode 100644 index 00000000..9b520e5f --- /dev/null +++ b/spec/item/type-specific-helpers/spell.spec.ts @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: MIT + +import { DS4SpellDataSourceData, TemporalUnit, UnitData } from "../../../src/item/item-data-source"; +import { calculateSpellPrice } from "../../../src/item/type-specific-helpers/spell"; + +const defaultData: DS4SpellDataSourceData = { + description: "", + equipped: false, + spellType: "spellcasting", + bonus: "", + spellCategory: "unset", + maxDistance: { + value: "", + unit: "meter", + }, + effectRadius: { + value: "", + unit: "meter", + }, + duration: { + value: "", + unit: "custom", + }, + cooldownDuration: { + value: "", + unit: "rounds", + }, + minimumLevels: { + healer: null, + wizard: null, + sorcerer: null, + }, +}; + +type TestCase = { + minimumLevel: number | null; + expected: number | null; +}; + +type CombinedTestCase = { + minimumLevels: DS4SpellDataSourceData["minimumLevels"]; + expected: number | null; +}; + +const testCases: Record = { + healer: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 45 }, + { minimumLevel: 3, expected: 80 }, + { minimumLevel: 4, expected: 115 }, + { minimumLevel: 5, expected: 150 }, + { minimumLevel: 6, expected: 185 }, + { minimumLevel: 7, expected: 220 }, + { minimumLevel: 8, expected: 255 }, + { minimumLevel: 9, expected: 290 }, + { minimumLevel: 10, expected: 325 }, + { minimumLevel: 11, expected: 360 }, + { minimumLevel: 12, expected: 395 }, + { minimumLevel: 13, expected: 430 }, + { minimumLevel: 14, expected: 465 }, + { minimumLevel: 15, expected: 500 }, + { minimumLevel: 16, expected: 535 }, + { minimumLevel: 17, expected: 570 }, + { minimumLevel: 18, expected: 605 }, + { minimumLevel: 19, expected: 640 }, + { minimumLevel: 20, expected: 675 }, + ], + sorcerer: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 75 }, + { minimumLevel: 3, expected: 140 }, + { minimumLevel: 4, expected: 205 }, + { minimumLevel: 5, expected: 270 }, + { minimumLevel: 6, expected: 335 }, + { minimumLevel: 7, expected: 400 }, + { minimumLevel: 8, expected: 465 }, + { minimumLevel: 9, expected: 530 }, + { minimumLevel: 10, expected: 595 }, + { minimumLevel: 11, expected: 660 }, + { minimumLevel: 12, expected: 725 }, + { minimumLevel: 13, expected: 790 }, + { minimumLevel: 14, expected: 855 }, + { minimumLevel: 15, expected: 920 }, + { minimumLevel: 16, expected: 985 }, + { minimumLevel: 17, expected: 1050 }, + { minimumLevel: 18, expected: 1115 }, + { minimumLevel: 19, expected: 1180 }, + { minimumLevel: 20, expected: 1245 }, + ], + wizard: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 60 }, + { minimumLevel: 3, expected: 110 }, + { minimumLevel: 4, expected: 160 }, + { minimumLevel: 5, expected: 210 }, + { minimumLevel: 6, expected: 260 }, + { minimumLevel: 7, expected: 310 }, + { minimumLevel: 8, expected: 360 }, + { minimumLevel: 9, expected: 410 }, + { minimumLevel: 10, expected: 460 }, + { minimumLevel: 11, expected: 510 }, + { minimumLevel: 12, expected: 560 }, + { minimumLevel: 13, expected: 610 }, + { minimumLevel: 14, expected: 660 }, + { minimumLevel: 15, expected: 710 }, + { minimumLevel: 16, expected: 760 }, + { minimumLevel: 17, expected: 810 }, + { minimumLevel: 18, expected: 860 }, + { minimumLevel: 19, expected: 910 }, + { minimumLevel: 20, expected: 960 }, + ], +}; + +function buildCombinedTestCases(): CombinedTestCase[] { + const combinedTestCases: CombinedTestCase[] = []; + + // permutation test cases + const isRelevantPermutationTestCase = (t: TestCase) => + ([null, 1, 10, 20] as (number | null)[]).includes(t.minimumLevel); + + for (const healerTestCase of testCases.healer.filter(isRelevantPermutationTestCase)) { + for (const sorcererTestCase of testCases.sorcerer.filter(isRelevantPermutationTestCase)) { + for (const wizardTestCase of testCases.wizard.filter(isRelevantPermutationTestCase)) { + const expected = + healerTestCase.expected !== null || + sorcererTestCase.expected !== null || + wizardTestCase.expected !== null + ? Math.min( + healerTestCase.expected ?? Infinity, + sorcererTestCase.expected ?? Infinity, + wizardTestCase.expected ?? Infinity, + ) + : null; + combinedTestCases.push({ + minimumLevels: { + healer: healerTestCase.minimumLevel, + sorcerer: sorcererTestCase.minimumLevel, + wizard: wizardTestCase.minimumLevel, + }, + expected, + }); + } + } + } + + // single test cases + const isRelevantSingleTestCase = (t: TestCase) => t.minimumLevel !== null; + + for (const spellCasterClass of ["healer", "sorcerer", "wizard"] as const) { + for (const testCase of testCases[spellCasterClass].filter(isRelevantSingleTestCase)) { + combinedTestCases.push({ + minimumLevels: { + ...defaultData.minimumLevels, + [spellCasterClass]: testCase.minimumLevel, + }, + expected: testCase.expected, + }); + } + } + + return combinedTestCases; +} + +describe("calculateSpellPrice", () => { + const cooldownDurations: (UnitData & { factor: number })[] = [ + { value: "", unit: "rounds", factor: 1 }, + { value: "foo", unit: "rounds", factor: 1 }, + { value: "0", unit: "rounds", factor: 1 }, + { value: "1", unit: "rounds", factor: 1 }, + { value: "17279", unit: "rounds", factor: 1 }, + { value: "17280", unit: "rounds", factor: 2 }, + { value: "34559", unit: "rounds", factor: 2 }, + { value: "34560", unit: "rounds", factor: 3 }, + + { value: "", unit: "minutes", factor: 1 }, + { value: "foo", unit: "minutes", factor: 1 }, + { value: "0", unit: "minutes", factor: 1 }, + { value: "1", unit: "minutes", factor: 1 }, + { value: "1439", unit: "minutes", factor: 1 }, + { value: "1440", unit: "minutes", factor: 2 }, + { value: "2879", unit: "minutes", factor: 2 }, + { value: "2880", unit: "minutes", factor: 3 }, + + { value: "", unit: "hours", factor: 1 }, + { value: "foo", unit: "hours", factor: 1 }, + { value: "0", unit: "hours", factor: 1 }, + { value: "1", unit: "hours", factor: 1 }, + { value: "23", unit: "hours", factor: 1 }, + { value: "24", unit: "hours", factor: 2 }, + { value: "47", unit: "hours", factor: 2 }, + { value: "48", unit: "hours", factor: 3 }, + + { value: "", unit: "days", factor: 3 }, + { value: "foo", unit: "days", factor: 3 }, + { value: "0", unit: "days", factor: 1 }, + { value: "1", unit: "days", factor: 2 }, + { value: "2", unit: "days", factor: 3 }, + ]; + + describe.each(cooldownDurations)("with cooldown duration set to $value $unit", ({ value, unit, factor }) => { + const dataWithCooldownDuration = { + ...defaultData, + cooldownDuration: { + value, + unit, + }, + }; + + it.each(buildCombinedTestCases())( + `returns ${factor} × $expected if the minimum leves are $minimumLevels`, + ({ minimumLevels, expected }) => { + // given + const data: DS4SpellDataSourceData = { + ...dataWithCooldownDuration, + minimumLevels, + }; + + // when + const spellPrice = calculateSpellPrice(data); + + // then + expect(spellPrice).toBe(expected !== null ? expected * factor : expected); + }, + ); + }); +}); diff --git a/spec/setup.ts b/spec/setup.ts index 9b10aba6..d96ad725 100644 --- a/spec/setup.ts +++ b/spec/setup.ts @@ -4,6 +4,35 @@ import en from "../lang/en.json"; +function setupPrimitives() { + Object.defineProperties(Number, { + isNumeric: { + value: function (n: unknown) { + if (n instanceof Array) return false; + else if (([null, ""] as unknown[]).includes(n)) return false; + // @ts-expect-error Abusing JavaScript a bit here, but it's the implementation from foundry + return +n === +n; + }, + }, + fromString: { + value: function (str: unknown) { + if (typeof str !== "string" || !str.length) return NaN; + // Remove whitespace. + str = str.replace(/\s+/g, ""); + return Number(str); + }, + }, + }); + + Object.defineProperties(Math, { + clamped: { + value: function (num: number, min: number, max: number) { + return Math.min(max, Math.max(num, min)); + }, + }, + }); +} + function setupStubs() { class StubGame { constructor() { @@ -24,4 +53,5 @@ function setupStubs() { }); } +setupPrimitives(); setupStubs();