forked from profectus/Profectus
Make requirements support buying max
This commit is contained in:
parent
757cfaa1ab
commit
7eeb0318e2
3 changed files with 137 additions and 70 deletions
|
@ -1992,7 +1992,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. Returns a ref of how many can be bought, as well as how much that will cost.
|
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
|
||||||
* @param formula The formula to use for calculating buy max from
|
* @param formula The formula to use for calculating buy max from
|
||||||
* @param resource The resource used when purchasing (is only read from)
|
* @param resource The resource used when purchasing (is only read from)
|
||||||
* @param spendResources Whether or not to count spent resources on each purchase or not
|
* @param spendResources Whether or not to count spent resources on each purchase or not
|
||||||
|
@ -2001,19 +2001,19 @@ export function calculateMaxAffordable(
|
||||||
formula: InvertibleFormula,
|
formula: InvertibleFormula,
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
spendResources?: true
|
spendResources?: true
|
||||||
): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> };
|
): ComputedRef<DecimalSource>;
|
||||||
export function calculateMaxAffordable(
|
export function calculateMaxAffordable(
|
||||||
formula: InvertibleIntegralFormula,
|
formula: InvertibleIntegralFormula,
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
spendResources: Computable<boolean>
|
spendResources: Computable<boolean>
|
||||||
): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> };
|
): ComputedRef<DecimalSource>;
|
||||||
export function calculateMaxAffordable(
|
export function calculateMaxAffordable(
|
||||||
formula: InvertibleFormula,
|
formula: InvertibleFormula,
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
spendResources: Computable<boolean> = true
|
spendResources: Computable<boolean> = true
|
||||||
) {
|
) {
|
||||||
const computedSpendResources = convertComputable(spendResources);
|
const computedSpendResources = convertComputable(spendResources);
|
||||||
const maxAffordable = computed(() => {
|
return computed(() => {
|
||||||
if (unref(computedSpendResources)) {
|
if (unref(computedSpendResources)) {
|
||||||
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
|
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
|
||||||
throw "Cannot calculate max affordable of formula with non-invertible integral";
|
throw "Cannot calculate max affordable of formula with non-invertible integral";
|
||||||
|
@ -2028,13 +2028,33 @@ export function calculateMaxAffordable(
|
||||||
return Decimal.floor((formula as InvertibleFormula).invert(resource.value));
|
return Decimal.floor((formula as InvertibleFormula).invert(resource.value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const cost = computed(() => {
|
}
|
||||||
const newValue = maxAffordable.value.add(unref(formula.innermostVariable) ?? 0);
|
|
||||||
if (unref(computedSpendResources)) {
|
/**
|
||||||
return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
|
* Utility for calculating the cost of a formula for a given amount of purchases. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
|
||||||
} else {
|
* @param formula The formula to use for calculating buy max from
|
||||||
return formula.evaluate(newValue);
|
* @param amountToBuy The amount of purchases to calculate the cost for
|
||||||
}
|
* @param spendResources Whether or not to count spent resources on each purchase or not
|
||||||
});
|
*/
|
||||||
return { maxAffordable, cost };
|
export function calculateCost(
|
||||||
|
formula: InvertibleFormula,
|
||||||
|
amountToBuy: DecimalSource,
|
||||||
|
spendResources?: true
|
||||||
|
): DecimalSource;
|
||||||
|
export function calculateCost(
|
||||||
|
formula: InvertibleIntegralFormula,
|
||||||
|
amountToBuy: DecimalSource,
|
||||||
|
spendResources: boolean
|
||||||
|
): DecimalSource;
|
||||||
|
export function calculateCost(
|
||||||
|
formula: InvertibleFormula,
|
||||||
|
amountToBuy: DecimalSource,
|
||||||
|
spendResources = true
|
||||||
|
) {
|
||||||
|
const newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
|
||||||
|
if (spendResources) {
|
||||||
|
return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
|
||||||
|
} else {
|
||||||
|
return formula.evaluate(newValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { joinJSX, renderJSX } from "util/vue";
|
import { joinJSX, renderJSX } from "util/vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from "./formulas";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that can be used to describe a requirement to perform some purchase or other action.
|
* An object that can be used to describe a requirement to perform some purchase or other action.
|
||||||
|
@ -18,36 +19,33 @@ import { computed, unref } from "vue";
|
||||||
*/
|
*/
|
||||||
export interface Requirement {
|
export interface Requirement {
|
||||||
/** The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. */
|
/** The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. */
|
||||||
partialDisplay?: JSXFunction;
|
partialDisplay?: (amount?: DecimalSource) => JSX.Element;
|
||||||
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
|
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
|
||||||
display?: JSXFunction;
|
display?: (amount?: DecimalSource) => JSX.Element;
|
||||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
|
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
|
||||||
requirementMet: ProcessedComputable<boolean>;
|
requirementMet: ProcessedComputable<DecimalSource | boolean>;
|
||||||
requiresPay: ProcessedComputable<boolean>;
|
requiresPay: ProcessedComputable<boolean>;
|
||||||
pay?: VoidFunction;
|
buyMax?: ProcessedComputable<boolean>;
|
||||||
|
pay?: (amount?: DecimalSource) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Requirements = Requirement | Requirement[];
|
export type Requirements = Requirement | Requirement[];
|
||||||
|
|
||||||
export interface CostRequirementOptions {
|
export interface CostRequirementOptions {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
cost: Computable<DecimalSource>;
|
cost: Computable<DecimalSource> | GenericFormula;
|
||||||
visibility?: Computable<Visibility.Visible | Visibility.None>;
|
visibility?: Computable<Visibility.Visible | Visibility.None>;
|
||||||
requiresPay?: ProcessedComputable<boolean>;
|
requiresPay?: Computable<boolean>;
|
||||||
pay?: VoidFunction;
|
buyMax?: Computable<boolean>;
|
||||||
|
spendResources?: Computable<boolean>;
|
||||||
|
pay?: (amount?: DecimalSource) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCostRequirement<T extends CostRequirementOptions>(
|
export function createCostRequirement<T extends CostRequirementOptions>(optionsFunc: () => T) {
|
||||||
optionsFunc: () => T
|
|
||||||
): Requirement {
|
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(() => {
|
||||||
const req = optionsFunc() as T & Partial<Requirement>;
|
const req = optionsFunc() as T & Partial<Requirement>;
|
||||||
|
|
||||||
req.requirementMet = computed(() =>
|
req.partialDisplay = amount => (
|
||||||
Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable<DecimalSource>))
|
|
||||||
);
|
|
||||||
|
|
||||||
req.partialDisplay = jsx(() => (
|
|
||||||
<span
|
<span
|
||||||
style={
|
style={
|
||||||
unref(req.requirementMet as ProcessedComputable<boolean>)
|
unref(req.requirementMet as ProcessedComputable<boolean>)
|
||||||
|
@ -57,33 +55,81 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
>
|
>
|
||||||
{displayResource(
|
{displayResource(
|
||||||
req.resource,
|
req.resource,
|
||||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
req.cost instanceof Formula
|
||||||
|
? calculateCost(
|
||||||
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(
|
||||||
|
req.spendResources as ProcessedComputable<boolean> | undefined
|
||||||
|
) ?? true
|
||||||
|
)
|
||||||
|
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{req.resource.displayName}
|
{req.resource.displayName}
|
||||||
</span>
|
</span>
|
||||||
));
|
);
|
||||||
req.display = jsx(() => (
|
req.display = amount => (
|
||||||
<div>
|
<div>
|
||||||
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
|
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
|
||||||
{displayResource(
|
{displayResource(
|
||||||
req.resource,
|
req.resource,
|
||||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
req.cost instanceof Formula
|
||||||
|
? calculateCost(
|
||||||
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(
|
||||||
|
req.spendResources as ProcessedComputable<boolean> | undefined
|
||||||
|
) ?? true
|
||||||
|
)
|
||||||
|
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{req.resource.displayName}
|
{req.resource.displayName}
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
|
|
||||||
processComputable(req as T, "visibility");
|
processComputable(req as T, "visibility");
|
||||||
setDefault(req, "visibility", Visibility.Visible);
|
setDefault(req, "visibility", Visibility.Visible);
|
||||||
processComputable(req as T, "cost");
|
processComputable(req as T, "cost");
|
||||||
processComputable(req as T, "requiresPay");
|
processComputable(req as T, "requiresPay");
|
||||||
|
processComputable(req as T, "spendResources");
|
||||||
setDefault(req, "requiresPay", true);
|
setDefault(req, "requiresPay", true);
|
||||||
setDefault(req, "pay", function () {
|
setDefault(req, "pay", function (amount?: DecimalSource) {
|
||||||
req.resource.value = Decimal.sub(
|
const cost =
|
||||||
req.resource.value,
|
req.cost instanceof Formula
|
||||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
? calculateCost(
|
||||||
).max(0);
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ??
|
||||||
|
true
|
||||||
|
)
|
||||||
|
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||||
|
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
||||||
});
|
});
|
||||||
|
processComputable(req as T, "buyMax");
|
||||||
|
|
||||||
|
if (
|
||||||
|
"buyMax" in req &&
|
||||||
|
req.buyMax !== false &&
|
||||||
|
req.cost instanceof Formula &&
|
||||||
|
req.cost.isInvertible()
|
||||||
|
) {
|
||||||
|
req.requirementMet = calculateMaxAffordable(
|
||||||
|
req.cost,
|
||||||
|
req.resource,
|
||||||
|
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
req.requirementMet = computed(() => {
|
||||||
|
if (req.cost instanceof Formula) {
|
||||||
|
return Decimal.gte(req.resource.value, req.cost.evaluate());
|
||||||
|
} else {
|
||||||
|
return Decimal.gte(
|
||||||
|
req.resource.value,
|
||||||
|
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return req as Requirement;
|
return req as Requirement;
|
||||||
});
|
});
|
||||||
|
@ -112,14 +158,26 @@ export function createBooleanRequirement(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requirementsMet(requirements: Requirements) {
|
export function requirementsMet(requirements: Requirements): boolean {
|
||||||
if (isArray(requirements)) {
|
if (isArray(requirements)) {
|
||||||
return requirements.every(r => unref(r.requirementMet));
|
return requirements.every(requirementsMet);
|
||||||
}
|
}
|
||||||
return unref(requirements.requirementMet);
|
const reqsMet = unref(requirements.requirementMet);
|
||||||
|
return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayRequirements(requirements: Requirements) {
|
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
||||||
|
if (isArray(requirements)) {
|
||||||
|
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
|
||||||
|
}
|
||||||
|
const reqsMet = unref(requirements.requirementMet);
|
||||||
|
if (typeof reqsMet === "boolean") {
|
||||||
|
return reqsMet ? Infinity : 0;
|
||||||
|
}
|
||||||
|
return reqsMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||||
if (isArray(requirements)) {
|
if (isArray(requirements)) {
|
||||||
requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible);
|
requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible);
|
||||||
if (requirements.length === 1) {
|
if (requirements.length === 1) {
|
||||||
|
@ -136,7 +194,7 @@ export function displayRequirements(requirements: Requirements) {
|
||||||
<div>
|
<div>
|
||||||
Costs:{" "}
|
Costs:{" "}
|
||||||
{joinJSX(
|
{joinJSX(
|
||||||
withCosts.map(r => r.partialDisplay!()),
|
withCosts.map(r => r.partialDisplay!(amount)),
|
||||||
<>, </>
|
<>, </>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -145,7 +203,7 @@ export function displayRequirements(requirements: Requirements) {
|
||||||
<div>
|
<div>
|
||||||
Requires:{" "}
|
Requires:{" "}
|
||||||
{joinJSX(
|
{joinJSX(
|
||||||
withoutCosts.map(r => r.partialDisplay!()),
|
withoutCosts.map(r => r.partialDisplay!(amount)),
|
||||||
<>, </>
|
<>, </>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,10 +214,11 @@ export function displayRequirements(requirements: Requirements) {
|
||||||
return requirements.display?.() ?? <></>;
|
return requirements.display?.() ?? <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function payRequirements(requirements: Requirements) {
|
export function payRequirements(requirements: Requirements, buyMax = false) {
|
||||||
|
const amount = buyMax ? maxRequirementsMet(requirements) : 1;
|
||||||
if (isArray(requirements)) {
|
if (isArray(requirements)) {
|
||||||
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.());
|
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
||||||
} else if (unref(requirements.requiresPay)) {
|
} else if (unref(requirements.requiresPay)) {
|
||||||
requirements.pay?.();
|
requirements.pay?.(amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createResource, Resource } from "features/resources/resource";
|
import { createResource, Resource } from "features/resources/resource";
|
||||||
import Formula, {
|
import Formula, {
|
||||||
|
calculateCost,
|
||||||
calculateMaxAffordable,
|
calculateMaxAffordable,
|
||||||
GenericFormula,
|
GenericFormula,
|
||||||
InvertibleFormula,
|
InvertibleFormula,
|
||||||
|
@ -983,45 +984,32 @@ describe("Buy Max", () => {
|
||||||
});
|
});
|
||||||
describe("With spending", () => {
|
describe("With spending", () => {
|
||||||
test("Throws on formula with non-invertible integral", () => {
|
test("Throws on formula with non-invertible integral", () => {
|
||||||
const { maxAffordable, cost } = calculateMaxAffordable(
|
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
|
||||||
Formula.neg(10),
|
|
||||||
resource,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(() => maxAffordable.value).toThrow();
|
expect(() => maxAffordable.value).toThrow();
|
||||||
expect(() => cost.value).toThrow();
|
|
||||||
});
|
});
|
||||||
// https://www.desmos.com/calculator/5vgletdc1p
|
// https://www.desmos.com/calculator/5vgletdc1p
|
||||||
test("Calculates max affordable and cost correctly", () => {
|
test("Calculates max affordable and cost correctly", () => {
|
||||||
const variable = Formula.variable(10);
|
const variable = Formula.variable(10);
|
||||||
const { maxAffordable, cost } = calculateMaxAffordable(
|
const formula = Formula.pow(1.05, variable);
|
||||||
Formula.pow(1.05, variable),
|
const maxAffordable = calculateMaxAffordable(formula, resource, false);
|
||||||
resource,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(maxAffordable.value).compare_tolerance(47);
|
expect(maxAffordable.value).compare_tolerance(47);
|
||||||
expect(cost.value).compare_tolerance(Decimal.pow(1.05, 47));
|
expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance(
|
||||||
|
Decimal.pow(1.05, 47)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("Without spending", () => {
|
describe("Without spending", () => {
|
||||||
test("Throws on non-invertible formula", () => {
|
test("Throws on non-invertible formula", () => {
|
||||||
const { maxAffordable, cost } = calculateMaxAffordable(
|
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false);
|
||||||
Formula.abs(10),
|
|
||||||
resource,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(() => maxAffordable.value).toThrow();
|
expect(() => maxAffordable.value).toThrow();
|
||||||
expect(() => cost.value).toThrow();
|
|
||||||
});
|
});
|
||||||
// https://www.desmos.com/calculator/5vgletdc1p
|
// https://www.desmos.com/calculator/5vgletdc1p
|
||||||
test("Calculates max affordable and cost correctly", () => {
|
test("Calculates max affordable and cost correctly", () => {
|
||||||
const variable = Formula.variable(10);
|
const variable = Formula.variable(10);
|
||||||
const { maxAffordable, cost } = calculateMaxAffordable(
|
const formula = Formula.pow(1.05, variable);
|
||||||
Formula.pow(1.05, variable),
|
const maxAffordable = calculateMaxAffordable(formula, resource);
|
||||||
resource
|
|
||||||
);
|
|
||||||
expect(maxAffordable.value).compare_tolerance(7);
|
expect(maxAffordable.value).compare_tolerance(7);
|
||||||
expect(cost.value).compare_tolerance(7.35);
|
expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue