Make conversions use formulas

This commit is contained in:
thepaperpilot 2023-04-02 20:30:08 -05:00
parent cc1a2998e0
commit 3e0aafcf02
2 changed files with 15 additions and 282 deletions

View file

@ -1,11 +1,10 @@
import type { OptionsFunc, Replace } from "features/feature"; import type { OptionsFunc, Replace } from "features/feature";
import { setDefault } from "features/feature"; import { setDefault } from "features/feature";
import type { Resource } from "features/resources/resource"; import type { Resource } from "features/resources/resource";
import { InvertibleFormula } from "game/formulas/types";
import type { BaseLayer } from "game/layers"; import type { BaseLayer } from "game/layers";
import type { Modifier } from "game/modifiers";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import Decimal from "util/bignum"; import Decimal from "util/bignum";
import type { WithRequired } from "util/common";
import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed";
import { convertComputable, processComputable } from "util/computed"; import { convertComputable, processComputable } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
@ -15,9 +14,10 @@ import { computed, unref } from "vue";
/** An object that configures a {@link Conversion}. */ /** An object that configures a {@link Conversion}. */
export interface ConversionOptions { 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. * How much of the output resource the conversion can currently convert for.
* Typically this will be set for you in a conversion constructor. * Typically this will be set for you in a conversion constructor.
@ -53,10 +53,6 @@ export interface ConversionOptions {
* Defaults to true. * Defaults to true.
*/ */
buyMax?: Computable<boolean>; buyMax?: Computable<boolean>;
/**
* Whether or not to round up the cost to generate a given amount of the output resource.
*/
roundUpCost?: Computable<boolean>;
/** /**
* The function that performs the actual conversion from {@link baseResource} to {@link gainResource}. * The function that performs the actual conversion from {@link baseResource} to {@link gainResource}.
* Typically this will be set for you in a conversion constructor. * 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) * This will not be called whenever using currentGain without calling convert (e.g. passive generation)
*/ */
onConvert?: (amountGained: DecimalSource) => void; 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<Modifier, "revert">;
/**
* 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<Modifier, "revert">;
} }
/** /**
@ -109,7 +91,6 @@ export type Conversion<T extends ConversionOptions> = Replace<
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>; nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>; buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"]; spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"];
roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
} }
>; >;
@ -123,7 +104,6 @@ export type GenericConversion = Replace<
nextAt: ProcessedComputable<DecimalSource>; nextAt: ProcessedComputable<DecimalSource>;
buyMax: ProcessedComputable<boolean>; buyMax: ProcessedComputable<boolean>;
spend: (amountGained: DecimalSource) => void; spend: (amountGained: DecimalSource) => void;
roundUpCost: ProcessedComputable<boolean>;
} }
>; >;
@ -142,11 +122,7 @@ export function createConversion<T extends ConversionOptions>(
if (conversion.currentGain == null) { if (conversion.currentGain == null) {
conversion.currentGain = computed(() => { conversion.currentGain = computed(() => {
let gain = conversion.gainModifier let gain = conversion.formula.evaluate(conversion.baseResource.value);
? conversion.gainModifier.apply(
conversion.scaling.currentGain(conversion as GenericConversion)
)
: conversion.scaling.currentGain(conversion as GenericConversion);
gain = Decimal.floor(gain).max(0); gain = Decimal.floor(gain).max(0);
if (unref(conversion.buyMax) === false) { if (unref(conversion.buyMax) === false) {
@ -160,17 +136,16 @@ export function createConversion<T extends ConversionOptions>(
} }
if (conversion.currentAt == null) { if (conversion.currentAt == null) {
conversion.currentAt = computed(() => { conversion.currentAt = computed(() => {
let current = conversion.scaling.currentAt(conversion as GenericConversion); return conversion.formula.invert(
if (unref((conversion as GenericConversion).roundUpCost)) Decimal.floor(unref((conversion as GenericConversion).currentGain))
current = Decimal.ceil(current); );
return current;
}); });
} }
if (conversion.nextAt == null) { if (conversion.nextAt == null) {
conversion.nextAt = computed(() => { conversion.nextAt = computed(() => {
let next = conversion.scaling.nextAt(conversion as GenericConversion); return conversion.formula.invert(
if (unref((conversion as GenericConversion).roundUpCost)) next = Decimal.ceil(next); Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1)
return next; );
}); });
} }
@ -198,177 +173,11 @@ export function createConversion<T extends ConversionOptions>(
processComputable(conversion as T, "nextAt"); processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax"); processComputable(conversion as T, "buyMax");
setDefault(conversion, "buyMax", true); setDefault(conversion, "buyMax", true);
processComputable(conversion as T, "roundUpCost");
setDefault(conversion, "roundUpCost", true);
return conversion as unknown as Conversion<T>; return conversion as unknown as Conversion<T>;
}); });
} }
/**
* 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<DecimalSource>,
coefficient: Computable<DecimalSource>
): 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<DecimalSource>,
exponent: Computable<DecimalSource>
): 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. * 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. * This is similar to the behavior of "normal" layers in The Modding Tree.
@ -396,13 +205,8 @@ export function createIndependentConversion<S extends ConversionOptions>(
if (conversion.currentGain == null) { if (conversion.currentGain == null) {
conversion.currentGain = computed(() => { conversion.currentGain = computed(() => {
let gain = conversion.gainModifier let gain = conversion.formula.evaluate(conversion.baseResource.value);
? conversion.gainModifier.apply(
conversion.scaling.currentGain(conversion as GenericConversion)
)
: conversion.scaling.currentGain(conversion as GenericConversion);
gain = Decimal.floor(gain).max(conversion.gainResource.value); gain = Decimal.floor(gain).max(conversion.gainResource.value);
if (unref(conversion.buyMax) === false) { if (unref(conversion.buyMax) === false) {
gain = gain.min(Decimal.add(conversion.gainResource.value, 1)); gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
} }
@ -412,7 +216,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
if (conversion.actualGain == null) { if (conversion.actualGain == null) {
conversion.actualGain = computed(() => { conversion.actualGain = computed(() => {
let gain = Decimal.sub( let gain = Decimal.sub(
Decimal.floor(conversion.scaling.currentGain(conversion as GenericConversion)), conversion.formula.evaluate(conversion.baseResource.value),
conversion.gainResource.value conversion.gainResource.value
).max(0); ).max(0);
@ -424,11 +228,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
} }
setDefault(conversion, "convert", function () { setDefault(conversion, "convert", function () {
const amountGained = unref((conversion as GenericConversion).actualGain); const amountGained = unref((conversion as GenericConversion).actualGain);
conversion.gainResource.value = conversion.gainModifier conversion.gainResource.value = unref((conversion as GenericConversion).currentGain);
? conversion.gainModifier.apply(
unref((conversion as GenericConversion).currentGain)
)
: unref((conversion as GenericConversion).currentGain);
(conversion as GenericConversion).spend(amountGained); (conversion as GenericConversion).spend(amountGained);
conversion.onConvert?.(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<DecimalSource>,
power: ProcessedComputable<DecimalSource> = 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<DecimalSource>
): ScalingFunction {
return {
...scaling,
currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap))
};
}

View file

@ -315,6 +315,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return new Formula({ variable: value }) as InvertibleFormula; 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. * 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. * This function assumes the incoming {@ref value} will be continuous and monotonically increasing.