Make conversions use formulas
This commit is contained in:
parent
cc1a2998e0
commit
3e0aafcf02
2 changed files with 15 additions and 282 deletions
|
@ -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<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}.
|
||||
* 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<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>>;
|
||||
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
|
||||
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>;
|
||||
buyMax: ProcessedComputable<boolean>;
|
||||
spend: (amountGained: DecimalSource) => void;
|
||||
roundUpCost: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -142,11 +122,7 @@ export function createConversion<T extends ConversionOptions>(
|
|||
|
||||
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<T extends ConversionOptions>(
|
|||
}
|
||||
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<T extends ConversionOptions>(
|
|||
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<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.
|
||||
* 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) {
|
||||
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<S extends ConversionOptions>(
|
|||
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<S extends ConversionOptions>(
|
|||
}
|
||||
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<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))
|
||||
};
|
||||
}
|
||||
|
|
|
@ -315,6 +315,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
|
|||
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.
|
||||
|
|
Loading…
Reference in a new issue