diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 97637fa..f5146ca 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -1,11 +1,10 @@ import type { OptionsFunc, Replace } from "features/feature"; import { setDefault } from "features/feature"; import type { Resource } from "features/resources/resource"; +import { InvertibleFormula } from "game/formulas/types"; import type { BaseLayer } from "game/layers"; -import type { Modifier } from "game/modifiers"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; -import type { WithRequired } from "util/common"; import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import { convertComputable, processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; @@ -15,9 +14,10 @@ import { computed, unref } from "vue"; /** An object that configures a {@link Conversion}. */ export interface ConversionOptions { /** - * The scaling function that is used to determine the rate of conversion from one {@link features/resources/resource.Resource} to the other. + * The formula used to determine how much {@link gainResource} should be earned by this converting. + * When evaluating, the variable will always be overidden to the amount of {@link baseResource}. */ - scaling: ScalingFunction; + formula: InvertibleFormula; /** * How much of the output resource the conversion can currently convert for. * Typically this will be set for you in a conversion constructor. @@ -53,10 +53,6 @@ export interface ConversionOptions { * Defaults to true. */ buyMax?: Computable; - /** - * Whether or not to round up the cost to generate a given amount of the output resource. - */ - roundUpCost?: Computable; /** * The function that performs the actual conversion from {@link baseResource} to {@link gainResource}. * Typically this will be set for you in a conversion constructor. @@ -73,20 +69,6 @@ export interface ConversionOptions { * This will not be called whenever using currentGain without calling convert (e.g. passive generation) */ onConvert?: (amountGained: DecimalSource) => void; - /** - * An additional modifier that will be applied to the gain amounts. - * Must be reversible in order to correctly calculate {@link nextAt}. - * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers. - */ - gainModifier?: WithRequired; - /** - * A modifier that will be applied to the cost amounts. - * That is to say, this modifier will be applied to the amount of baseResource before going into the scaling function. - * A cost modifier of x0.5 would give gain amounts equal to the player having half the baseResource they actually have. - * Must be reversible in order to correctly calculate {@link nextAt}. - * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers. - */ - costModifier?: WithRequired; } /** @@ -109,7 +91,6 @@ export type Conversion = Replace< nextAt: GetComputableTypeWithDefault>; buyMax: GetComputableTypeWithDefault; spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"]; - roundUpCost: GetComputableTypeWithDefault; } >; @@ -123,7 +104,6 @@ export type GenericConversion = Replace< nextAt: ProcessedComputable; buyMax: ProcessedComputable; spend: (amountGained: DecimalSource) => void; - roundUpCost: ProcessedComputable; } >; @@ -142,11 +122,7 @@ export function createConversion( if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.gainModifier - ? conversion.gainModifier.apply( - conversion.scaling.currentGain(conversion as GenericConversion) - ) - : conversion.scaling.currentGain(conversion as GenericConversion); + let gain = conversion.formula.evaluate(conversion.baseResource.value); gain = Decimal.floor(gain).max(0); if (unref(conversion.buyMax) === false) { @@ -160,17 +136,16 @@ export function createConversion( } if (conversion.currentAt == null) { conversion.currentAt = computed(() => { - let current = conversion.scaling.currentAt(conversion as GenericConversion); - if (unref((conversion as GenericConversion).roundUpCost)) - current = Decimal.ceil(current); - return current; + return conversion.formula.invert( + Decimal.floor(unref((conversion as GenericConversion).currentGain)) + ); }); } if (conversion.nextAt == null) { conversion.nextAt = computed(() => { - let next = conversion.scaling.nextAt(conversion as GenericConversion); - if (unref((conversion as GenericConversion).roundUpCost)) next = Decimal.ceil(next); - return next; + return conversion.formula.invert( + Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1) + ); }); } @@ -198,177 +173,11 @@ export function createConversion( processComputable(conversion as T, "nextAt"); processComputable(conversion as T, "buyMax"); setDefault(conversion, "buyMax", true); - processComputable(conversion as T, "roundUpCost"); - setDefault(conversion, "roundUpCost", true); return conversion as unknown as Conversion; }); } -/** - * A collection of functions that allow a conversion to scale the amount of resources gained based on the input resource. - * This typically shouldn't be created directly. Instead use one of the scaling function constructors. - * @see {@link createLinearScaling}. - * @see {@link createPolynomialScaling}. - */ -export interface ScalingFunction { - /** - * Calculates the amount of the output resource a conversion should be able to currently produce. - * This should be based off of `conversion.baseResource.value`. - * The conversion is responsible for applying the gainModifier, so this function should be un-modified. - * It does not need to be clamped or rounded. - */ - currentGain: (conversion: GenericConversion) => DecimalSource; - /** - * Calculates the amount of the input resource that is required for the current value of `conversion.currentGain`. - * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate. - * The conversion is responsible for rounding up the amount as appropriate. - * The returned value should not be below 0. - */ - currentAt: (conversion: GenericConversion) => DecimalSource; - /** - * Calculates the amount of the input resource that would be required for the current value of `conversion.currentGain` to increase. - * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate. - * The conversion is responsible for rounding up the amount as appropriate. - * The returned value should not be below 0. - */ - nextAt: (conversion: GenericConversion) => DecimalSource; -} - -/** - * Creates a scaling function based off the formula `(baseResource - base) * coefficient`. - * If the baseResource value is less than base then the currentGain will be 0. - * @param base The base variable in the scaling formula. - * @param coefficient The coefficient variable in the scaling formula. - * @example - * A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values: - * | Base Resource | Current Gain | - * | ------------- | ------------ | - * | 10 | 1 | - * | 12 | 2 | - * | 20 | 6 | - */ -export function createLinearScaling( - base: Computable, - coefficient: Computable -): ScalingFunction { - const processedBase = convertComputable(base); - const processedCoefficient = convertComputable(coefficient); - return { - currentGain(conversion) { - let baseAmount: DecimalSource = unref(conversion.baseResource.value); - if (conversion.costModifier) { - baseAmount = conversion.costModifier.apply(baseAmount); - } - if (Decimal.lt(baseAmount, unref(processedBase))) { - return 0; - } - - return Decimal.sub(baseAmount, unref(processedBase)) - .sub(1) - .times(unref(processedCoefficient)) - .add(1); - }, - currentAt(conversion) { - let current: DecimalSource = unref(conversion.currentGain); - if (conversion.gainModifier) { - current = conversion.gainModifier.revert(current); - } - current = Decimal.max(0, current) - .sub(1) - .div(unref(processedCoefficient)) - .add(unref(processedBase)); - if (conversion.costModifier) { - current = conversion.costModifier.revert(current); - } - return current; - }, - nextAt(conversion) { - let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor(); - if (conversion.gainModifier) { - next = conversion.gainModifier.revert(next); - } - next = Decimal.max(0, next) - .sub(1) - .div(unref(processedCoefficient)) - .add(unref(processedBase)) - .max(unref(processedBase)); - if (conversion.costModifier) { - next = conversion.costModifier.revert(next); - } - return next; - } - }; -} - -/** - * Creates a scaling function based off the formula `(baseResource / base) ^ exponent`. - * If the baseResource value is less than base then the currentGain will be 0. - * @param base The base variable in the scaling formula. - * @param exponent The exponent variable in the scaling formula. - * @example - * A scaling function created via `createPolynomialScaling(10, 0.5)` would produce the following values: - * | Base Resource | Current Gain | - * | ------------- | ------------ | - * | 10 | 1 | - * | 40 | 2 | - * | 250 | 5 | - */ -export function createPolynomialScaling( - base: Computable, - exponent: Computable -): ScalingFunction { - const processedBase = convertComputable(base); - const processedExponent = convertComputable(exponent); - return { - currentGain(conversion) { - let baseAmount: DecimalSource = unref(conversion.baseResource.value); - if (conversion.costModifier) { - baseAmount = conversion.costModifier.apply(baseAmount); - } - if (Decimal.lt(baseAmount, unref(processedBase))) { - return 0; - } - - const gain = Decimal.div(baseAmount, unref(processedBase)).pow( - unref(processedExponent) - ); - - if (gain.isNan()) { - return new Decimal(0); - } - return gain; - }, - currentAt(conversion) { - let current: DecimalSource = unref(conversion.currentGain); - if (conversion.gainModifier) { - current = conversion.gainModifier.revert(current); - } - current = Decimal.max(0, current) - .root(unref(processedExponent)) - .times(unref(processedBase)); - if (conversion.costModifier) { - current = conversion.costModifier.revert(current); - } - return current; - }, - nextAt(conversion) { - let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor(); - if (conversion.gainModifier) { - next = conversion.gainModifier.revert(next); - } - next = Decimal.max(0, next) - .root(unref(processedExponent)) - .times(unref(processedBase)) - .max(unref(processedBase)); - if (conversion.costModifier) { - next = conversion.costModifier.revert(next); - } - return next; - } - }; -} - /** * Creates a conversion that simply adds to the gainResource amount upon converting. * This is similar to the behavior of "normal" layers in The Modding Tree. @@ -396,13 +205,8 @@ export function createIndependentConversion( if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.gainModifier - ? conversion.gainModifier.apply( - conversion.scaling.currentGain(conversion as GenericConversion) - ) - : conversion.scaling.currentGain(conversion as GenericConversion); + let gain = conversion.formula.evaluate(conversion.baseResource.value); gain = Decimal.floor(gain).max(conversion.gainResource.value); - if (unref(conversion.buyMax) === false) { gain = gain.min(Decimal.add(conversion.gainResource.value, 1)); } @@ -412,7 +216,7 @@ export function createIndependentConversion( if (conversion.actualGain == null) { conversion.actualGain = computed(() => { let gain = Decimal.sub( - Decimal.floor(conversion.scaling.currentGain(conversion as GenericConversion)), + conversion.formula.evaluate(conversion.baseResource.value), conversion.gainResource.value ).max(0); @@ -424,11 +228,7 @@ export function createIndependentConversion( } setDefault(conversion, "convert", function () { const amountGained = unref((conversion as GenericConversion).actualGain); - conversion.gainResource.value = conversion.gainModifier - ? conversion.gainModifier.apply( - unref((conversion as GenericConversion).currentGain) - ) - : unref((conversion as GenericConversion).currentGain); + conversion.gainResource.value = unref((conversion as GenericConversion).currentGain); (conversion as GenericConversion).spend(amountGained); conversion.onConvert?.(amountGained); }); @@ -466,71 +266,3 @@ export function setupPassiveGeneration( } }); } - -/** - * Given a value, this function finds the amount above a certain value and raises it to a power. - * If the power is <1, this will effectively make the value scale slower after the cap. - * @param value The raw value. - * @param cap The value after which the softcap should be applied. - * @param power The power to raise value above the cap to. - * @example - * A softcap added via `addSoftcap(scaling, 100, 0.5)` would produce the following values: - * | Raw Value | Softcapped Value | - * | --------- | ---------------- | - * | 1 | 1 | - * | 100 | 100 | - * | 125 | 105 | - * | 200 | 110 | - */ -export function softcap( - value: DecimalSource, - cap: DecimalSource, - power: DecimalSource = 0.5 -): DecimalSource { - if (Decimal.lte(value, cap)) { - return value; - } else { - return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power))); - } -} - -/** - * Creates a scaling function based off an existing scaling function, with a softcap applied to it. - * The softcap will take any value above a certain value and raise it to a power. - * If the power is <1, this will effectively make the value scale slower after the cap. - * @param scaling The raw scaling function. - * @param cap The value after which the softcap should be applied. - * @param power The power to raise value about the cap to. - * @see {@link softcap}. - */ -export function addSoftcap( - scaling: ScalingFunction, - cap: ProcessedComputable, - power: ProcessedComputable = 0.5 -): ScalingFunction { - return { - ...scaling, - currentAt: conversion => - softcap(scaling.currentAt(conversion), unref(cap), Decimal.recip(unref(power))), - nextAt: conversion => - softcap(scaling.nextAt(conversion), unref(cap), Decimal.recip(unref(power))), - currentGain: conversion => - softcap(scaling.currentGain(conversion), unref(cap), unref(power)) - }; -} - -/** - * Creates a scaling function off an existing function, with a hardcap applied to it. - * The harcap will ensure that the currentGain will stop at a given cap. - * @param scaling The raw scaling function. - * @param cap The maximum value the scaling function can output. - */ -export function addHardcap( - scaling: ScalingFunction, - cap: ProcessedComputable -): ScalingFunction { - return { - ...scaling, - currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap)) - }; -} diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 12ea545..9806580 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -315,6 +315,7 @@ export default class Formula { return new Formula({ variable: value }) as InvertibleFormula; } + // TODO add integration support to step-wise functions /** * 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.