diff --git a/src/game/formulas.ts b/src/game/formulas.ts index e4d5af8..7453c05 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -432,6 +432,46 @@ export default class Formula { ); } + /** + * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier. + * This function assumes the incoming {@ref value} will be continuous and monotonically increasing. + * @param value The value before applying the step + * @param start The value at which to start applying the step + * @param formulaModifier How this step should modify the formula. The incoming value will be the unmodified formula value _minus the start value_. So for example if an incoming formula evaluates to 200 and has a step that starts at 150, the formulaModifier would be given 50 as the parameter + */ + public static step( + value: T, + start: ProcessedComputable, + formulaModifier: (value: Ref) => GenericFormula + ) { + const lhsRef = ref(0); + const formula = formulaModifier(lhsRef); + function evalStep(lhs: DecimalSource) { + if (Decimal.lt(lhs, unref(start))) { + return lhs; + } + lhsRef.value = Decimal.sub(lhs, unref(start)); + return Decimal.add(formula.evaluate(), unref(start)); + } + function invertStep(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + if (Decimal.gt(value, unref(start))) { + value = Decimal.add( + formula.invert(Decimal.sub(value, unref(start))), + unref(start) + ); + } + return lhs.invert(value); + } + throw "Could not invert due to no input being a variable"; + } + return new Formula( + [value], + evalStep, + formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + ); + } + public static constant(value: InvertibleFormulaSource): InvertibleFormula { return new Formula([value]) as InvertibleFormula; } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 484bd34..cb5f4d4 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,7 +1,7 @@ import Formula, { GenericFormula, InvertibleFormula, unrefFormulaSource } from "game/formulas"; import Decimal, { DecimalSource, format } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { ref } from "vue"; +import { Ref, ref } from "vue"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -53,10 +53,10 @@ function testConstant( beforeAll(() => { formula = formulaFunc(); }); - test("evaluates correctly", () => + test("Evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(expectedValue)); - test("invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); - test("is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); + test("Invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); + test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); }); } @@ -475,3 +475,58 @@ describe("Variables", () => { ); }); }); + +describe("Step-wise", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("Formula without variable is marked as such", () => { + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).hasVariable()).toBe(false); + }); + + test("Formula with variable is marked as such", () => { + expect(Formula.step(variable, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(variable, 10, value => Formula.sqrt(value)).hasVariable()).toBe(true); + }); + + test("Non-invertible formula modifier marks formula as such", () => { + expect(Formula.step(constant, 10, value => Formula.abs(value)).isInvertible()).toBe(false); + expect(Formula.step(constant, 10, value => Formula.abs(value)).hasVariable()).toBe(false); + }); + + test("Formula modifiers with variables mark formula as non-invertible", () => { + expect( + Formula.step(constant, 10, value => Formula.add(value, variable)).isInvertible() + ).toBe(false); + expect( + Formula.step(constant, 10, value => Formula.add(value, variable)).hasVariable() + ).toBe(false); + }); + + describe("Pass-through underneath start", () => { + test("Evaluates correctly", () => + expect( + Formula.step(constant, 20, value => Formula.sqrt(value)).evaluate() + ).compare_tolerance(10)); + test("Inverts correctly with variable in input", () => + expect( + Formula.step(variable, 20, value => Formula.sqrt(value)).invert(10) + ).compare_tolerance(10)); + }); + + describe("Evaluates correctly beyond start", () => { + test("Evaluates correctly", () => + expect( + Formula.step(variable, 8, value => Formula.add(value, 2)).evaluate() + ).compare_tolerance(12)); + test("Inverts correctly", () => + expect( + Formula.step(variable, 8, value => Formula.add(value, 2)).invert(12) + ).compare_tolerance(10)); + }); +});