Profectus-Demo/src/features/conversion.ts

305 lines
11 KiB
TypeScript
Raw Normal View History

2022-03-04 03:39:48 +00:00
import { GenericLayer } from "game/layers";
2022-04-12 03:50:02 +00:00
import { Modifier } from "game/modifiers";
2022-03-04 03:39:48 +00:00
import Decimal, { DecimalSource } from "util/bignum";
2022-01-14 04:25:47 +00:00
import {
Computable,
GetComputableTypeWithDefault,
processComputable,
ProcessedComputable
2022-03-04 03:39:48 +00:00
} from "util/computed";
import { createLazyProxy } from "util/proxies";
2022-01-14 04:25:47 +00:00
import { computed, isRef, Ref, unref } from "vue";
import { OptionsFunc, Replace, setDefault } from "./feature";
2022-02-27 22:04:56 +00:00
import { Resource } from "./resources/resource";
2022-01-14 04:25:47 +00:00
export interface ConversionOptions {
scaling: ScalingFunction;
currentGain?: Computable<DecimalSource>;
2022-04-10 22:05:49 +00:00
actualGain?: Computable<DecimalSource>;
currentAt?: Computable<DecimalSource>;
2022-01-14 04:25:47 +00:00
nextAt?: Computable<DecimalSource>;
baseResource: Resource;
gainResource: Resource;
buyMax?: Computable<boolean>;
roundUpCost?: Computable<boolean>;
convert?: VoidFunction;
2022-04-12 03:50:02 +00:00
gainModifier?: Modifier;
2022-01-14 04:25:47 +00:00
}
2022-03-09 01:40:51 +00:00
export interface BaseConversion {
2022-01-14 04:25:47 +00:00
convert: VoidFunction;
}
export type Conversion<T extends ConversionOptions> = Replace<
T & BaseConversion,
{
currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
2022-04-10 22:05:49 +00:00
actualGain: GetComputableTypeWithDefault<T["actualGain"], Ref<DecimalSource>>;
currentAt: GetComputableTypeWithDefault<T["currentAt"], Ref<DecimalSource>>;
2022-01-14 04:25:47 +00:00
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>;
2022-04-10 22:05:49 +00:00
actualGain: ProcessedComputable<DecimalSource>;
currentAt: ProcessedComputable<DecimalSource>;
2022-01-14 04:25:47 +00:00
nextAt: ProcessedComputable<DecimalSource>;
buyMax: ProcessedComputable<boolean>;
roundUpCost: ProcessedComputable<boolean>;
}
>;
export function createConversion<T extends ConversionOptions>(
optionsFunc: OptionsFunc<T, Conversion<T>, BaseConversion>
2022-01-14 04:25:47 +00:00
): Conversion<T> {
return createLazyProxy(() => {
const conversion = optionsFunc();
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);
gain = Decimal.floor(gain).max(0);
if (!unref(conversion.buyMax)) {
gain = gain.min(1);
}
return gain;
});
}
2022-04-10 22:05:49 +00:00
if (conversion.actualGain == null) {
conversion.actualGain = conversion.currentGain;
}
if (conversion.currentAt == null) {
conversion.currentAt = computed(() => {
let current = conversion.scaling.currentAt(conversion as GenericConversion);
if (conversion.roundUpCost) current = Decimal.ceil(current);
return current;
});
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() => {
let next = conversion.scaling.nextAt(conversion as GenericConversion);
if (conversion.roundUpCost) next = Decimal.ceil(next);
return next;
});
}
2022-01-14 04:25:47 +00:00
if (conversion.convert == null) {
conversion.convert = function () {
conversion.gainResource.value = Decimal.add(
conversion.gainResource.value,
unref((conversion as GenericConversion).currentGain)
);
// TODO just subtract cost?
conversion.baseResource.value = 0;
};
}
2022-01-14 04:25:47 +00:00
processComputable(conversion as T, "currentGain");
2022-04-10 22:05:49 +00:00
processComputable(conversion as T, "actualGain");
processComputable(conversion as T, "currentAt");
processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax");
setDefault(conversion, "buyMax", true);
processComputable(conversion as T, "roundUpCost");
setDefault(conversion, "roundUpCost", true);
2022-01-14 04:25:47 +00:00
return conversion as unknown as Conversion<T>;
});
2022-01-14 04:25:47 +00:00
}
export type ScalingFunction = {
currentGain: (conversion: GenericConversion) => DecimalSource;
2022-04-10 22:05:49 +00:00
currentAt: (conversion: GenericConversion) => DecimalSource;
2022-01-14 04:25:47 +00:00
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
2022-01-14 04:25:47 +00:00
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;
}
return Decimal.sub(conversion.baseResource.value, unref(base))
.sub(1)
.times(unref(coefficient))
.add(1);
2022-01-14 04:25:47 +00:00
},
2022-04-10 22:05:49 +00:00
currentAt(conversion) {
let current: DecimalSource = unref(conversion.currentGain);
if (conversion.gainModifier) {
current = conversion.gainModifier.revert(current);
}
current = Decimal.max(0, current);
2022-04-10 22:05:49 +00:00
return Decimal.times(current, unref(coefficient)).add(unref(base));
},
2022-01-14 04:25:47 +00:00
nextAt(conversion) {
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
if (conversion.gainModifier) {
next = conversion.gainModifier.revert(next);
}
next = Decimal.max(0, next);
return Decimal.times(next, unref(coefficient)).add(unref(base)).max(unref(base));
2022-01-14 04:25:47 +00:00
}
};
}
// 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 createPolynomialScaling(
2022-01-14 04:25:47 +00:00
base: DecimalSource | Ref<DecimalSource>,
exponent: DecimalSource | Ref<DecimalSource>
): ScalingFunction {
return {
currentGain(conversion) {
const gain = Decimal.div(conversion.baseResource.value, unref(base)).pow(
unref(exponent)
);
2022-01-14 04:25:47 +00:00
if (gain.isNan()) {
return new Decimal(0);
}
2022-01-14 04:25:47 +00:00
return gain;
},
2022-04-10 22:05:49 +00:00
currentAt(conversion) {
let current: DecimalSource = unref(conversion.currentGain);
if (conversion.gainModifier) {
current = conversion.gainModifier.revert(current);
}
current = Decimal.max(0, current);
2022-04-10 22:05:49 +00:00
return Decimal.root(current, unref(exponent)).times(unref(base));
},
2022-01-14 04:25:47 +00:00
nextAt(conversion) {
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
if (conversion.gainModifier) {
next = conversion.gainModifier.revert(next);
}
next = Decimal.max(0, next);
return Decimal.root(next, unref(exponent)).times(unref(base)).max(unref(base));
2022-01-14 04:25:47 +00:00
}
};
}
export function createCumulativeConversion<S extends ConversionOptions>(
optionsFunc: OptionsFunc<S, Conversion<S>>
2022-01-14 04:25:47 +00:00
): Conversion<S> {
return createConversion(optionsFunc);
2022-01-14 04:25:47 +00:00
}
export function createIndependentConversion<S extends ConversionOptions>(
optionsFunc: OptionsFunc<S, Conversion<S>>
2022-01-14 04:25:47 +00:00
): Conversion<S> {
return createConversion(() => {
const conversion: S = optionsFunc();
setDefault(conversion, "buyMax", false);
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);
gain = Decimal.floor(gain).max(conversion.gainResource.value);
if (!unref(conversion.buyMax)) {
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
}
return gain;
});
}
2022-04-10 22:05:49 +00:00
if (conversion.actualGain == null) {
conversion.actualGain = computed(() => {
let gain = Decimal.sub(
conversion.scaling.currentGain(conversion as GenericConversion),
conversion.gainResource.value
).max(0);
if (!unref(conversion.buyMax)) {
gain = gain.min(1);
}
return gain;
});
}
setDefault(conversion, "convert", function () {
conversion.gainResource.value = conversion.gainModifier
? conversion.gainModifier.apply(
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;
});
2022-01-14 04:25:47 +00:00
return conversion;
2022-01-14 04:25:47 +00:00
});
}
export function setupPassiveGeneration(
layer: GenericLayer,
conversion: GenericConversion,
rate: ProcessedComputable<DecimalSource> = 1
2022-01-14 04:25:47 +00:00
): void {
layer.on("preUpdate", diff => {
2022-01-14 04:25:47 +00:00
const currRate = isRef(rate) ? rate.value : rate;
if (Decimal.neq(currRate, 0)) {
conversion.gainResource.value = Decimal.add(
conversion.gainResource.value,
2022-01-14 04:25:47 +00:00
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))
};
}