Profectus/src/features/conversion.ts

253 lines
8.2 KiB
TypeScript

import { GenericLayer } from "@/game/layers";
import Decimal, { DecimalSource } from "@/util/bignum";
import {
Computable,
GetComputableTypeWithDefault,
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createLazyProxy } from "@/util/proxies";
import { computed, isRef, Ref, unref } from "vue";
import { Replace, setDefault } from "./feature";
import { Resource } from "./resources/resource";
export interface ConversionOptions {
scaling: ScalingFunction;
currentGain?: Computable<DecimalSource>;
nextAt?: Computable<DecimalSource>;
baseResource: Resource;
gainResource: Resource;
buyMax?: Computable<boolean>;
roundUpCost?: Computable<boolean>;
convert?: VoidFunction;
modifyGainAmount?: (gain: DecimalSource) => DecimalSource;
}
interface BaseConversion {
convert: VoidFunction;
}
export type Conversion<T extends ConversionOptions> = Replace<
T & BaseConversion,
{
currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
}
>;
export type GenericConversion = Replace<
Conversion<ConversionOptions>,
{
currentGain: ProcessedComputable<DecimalSource>;
nextAt: ProcessedComputable<DecimalSource>;
buyMax: ProcessedComputable<boolean>;
roundUpCost: ProcessedComputable<boolean>;
}
>;
export function createConversion<T extends ConversionOptions>(
optionsFunc: () => T & ThisType<Conversion<T>>
): Conversion<T> {
return createLazyProxy(() => {
const conversion: T = optionsFunc();
if (conversion.currentGain == null) {
conversion.currentGain = computed(() =>
conversion.scaling.currentGain(conversion as GenericConversion)
);
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() =>
conversion.scaling.nextAt(conversion as GenericConversion)
);
}
if (conversion.convert == null) {
conversion.convert = function () {
conversion.gainResource.value = Decimal.add(
conversion.gainResource.value,
conversion.modifyGainAmount
? conversion.modifyGainAmount(
unref((conversion as GenericConversion).currentGain)
)
: unref((conversion as GenericConversion).currentGain)
);
// TODO just subtract cost?
conversion.baseResource.value = 0;
};
}
processComputable(conversion as T, "currentGain");
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>;
});
}
export type ScalingFunction = {
currentGain: (conversion: GenericConversion) => DecimalSource;
nextAt: (conversion: GenericConversion) => DecimalSource;
};
// Gain formula is (baseResource - base) * coefficient
// e.g. if base is 10 and coefficient is 0.5, 10 points makes 1 gain, 12 points is 2
export function createLinearScaling(
base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource>
): ScalingFunction {
return {
currentGain(conversion) {
if (Decimal.lt(conversion.baseResource.value, unref(base))) {
return 0;
}
let gain = Decimal.sub(conversion.baseResource.value, unref(base))
.sub(1)
.times(unref(coefficient))
.add(1)
.floor()
.max(0);
if (!conversion.buyMax) {
gain = gain.min(1);
}
return gain;
},
nextAt(conversion) {
let next = Decimal.add(unref(conversion.currentGain), 1)
.times(unref(coefficient))
.add(unref(base))
.max(unref(base));
if (conversion.roundUpCost) next = next.ceil();
return next;
}
};
}
// Gain formula is (baseResource / base) ^ exponent
// e.g. if exponent is 0.5 and base is 10, then having 10 points makes gain 1, and 40 points is 2
export function createExponentialScaling(
base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource>,
exponent: DecimalSource | Ref<DecimalSource>
): ScalingFunction {
return {
currentGain(conversion) {
let gain = Decimal.div(conversion.baseResource.value, unref(base))
.pow(unref(exponent))
.floor()
.max(0);
if (gain.isNan()) {
return new Decimal(0);
}
if (!conversion.buyMax) {
gain = gain.min(1);
}
return gain;
},
nextAt(conversion) {
let next = Decimal.add(unref(conversion.currentGain), 1)
.root(unref(exponent))
.times(unref(base))
.max(unref(base));
if (conversion.roundUpCost) next = next.ceil();
return next;
}
};
}
export function createCumulativeConversion<S extends ConversionOptions>(
optionsFunc: () => S & ThisType<Conversion<S>>
): Conversion<S> {
return createConversion(optionsFunc);
}
export function createIndependentConversion<S extends ConversionOptions>(
optionsFunc: () => S & ThisType<Conversion<S>>
): Conversion<S> {
return createConversion(() => {
const conversion: S = optionsFunc();
setDefault(conversion, "buyMax", false);
if (conversion.currentGain == null) {
conversion.currentGain = computed(() =>
Decimal.sub(
conversion.scaling.currentGain(conversion as GenericConversion),
conversion.gainResource.value
)
.add(1)
.max(1)
);
}
setDefault(conversion, "convert", function () {
conversion.gainResource.value = conversion.modifyGainAmount
? conversion.modifyGainAmount(unref((conversion as GenericConversion).currentGain))
: unref((conversion as GenericConversion).currentGain);
// TODO just subtract cost?
// Maybe by adding a cost function to scaling and nextAt just calls the cost function
// with 1 + currentGain
conversion.baseResource.value = 0;
});
return conversion;
});
}
export function setupPassiveGeneration(
layer: GenericLayer,
conversion: GenericConversion,
rate: ProcessedComputable<DecimalSource> = 1
): void {
layer.on("preUpdate", (diff: Decimal) => {
const currRate = isRef(rate) ? rate.value : rate;
if (Decimal.neq(currRate, 0)) {
conversion.gainResource.value = Decimal.add(
conversion.gainResource.value,
Decimal.times(currRate, diff).times(unref(conversion.currentGain))
);
}
});
}
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)));
}
}
export function addSoftcap(
scaling: ScalingFunction,
cap: ProcessedComputable<DecimalSource>,
power: ProcessedComputable<DecimalSource> = 0.5
): ScalingFunction {
return {
...scaling,
currentGain: conversion =>
softcap(scaling.currentGain(conversion), unref(cap), unref(power))
};
}
export function addHardcap(
scaling: ScalingFunction,
cap: ProcessedComputable<DecimalSource>
): ScalingFunction {
return {
...scaling,
currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap))
};
}