diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 0c95ba2..3536123 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1992,7 +1992,7 @@ export default class Formula { } /** - * 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 resource The resource used when purchasing (is only read from) * @param spendResources Whether or not to count spent resources on each purchase or not @@ -2001,19 +2001,19 @@ export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, spendResources?: true -): { maxAffordable: ComputedRef; cost: ComputedRef }; +): ComputedRef; export function calculateMaxAffordable( formula: InvertibleIntegralFormula, resource: Resource, spendResources: Computable -): { maxAffordable: ComputedRef; cost: ComputedRef }; +): ComputedRef; export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, spendResources: Computable = true ) { const computedSpendResources = convertComputable(spendResources); - const maxAffordable = computed(() => { + return computed(() => { if (unref(computedSpendResources)) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { 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)); } }); - const cost = computed(() => { - const newValue = maxAffordable.value.add(unref(formula.innermostVariable) ?? 0); - if (unref(computedSpendResources)) { - return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); - } else { - return formula.evaluate(newValue); - } - }); - return { maxAffordable, cost }; +} + +/** + * 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. + * @param formula The formula to use for calculating buy max from + * @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 + */ +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); + } } diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 607800b..b4126fc 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -11,6 +11,7 @@ import { import { createLazyProxy } from "util/proxies"; import { joinJSX, renderJSX } from "util/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. @@ -18,36 +19,33 @@ import { computed, unref } from "vue"; */ 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}. */ - partialDisplay?: JSXFunction; + partialDisplay?: (amount?: DecimalSource) => JSX.Element; /** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */ - display?: JSXFunction; + display?: (amount?: DecimalSource) => JSX.Element; visibility: ProcessedComputable; - requirementMet: ProcessedComputable; + requirementMet: ProcessedComputable; requiresPay: ProcessedComputable; - pay?: VoidFunction; + buyMax?: ProcessedComputable; + pay?: (amount?: DecimalSource) => void; } export type Requirements = Requirement | Requirement[]; export interface CostRequirementOptions { resource: Resource; - cost: Computable; + cost: Computable | GenericFormula; visibility?: Computable; - requiresPay?: ProcessedComputable; - pay?: VoidFunction; + requiresPay?: Computable; + buyMax?: Computable; + spendResources?: Computable; + pay?: (amount?: DecimalSource) => void; } -export function createCostRequirement( - optionsFunc: () => T -): Requirement { +export function createCostRequirement(optionsFunc: () => T) { return createLazyProxy(() => { const req = optionsFunc() as T & Partial; - req.requirementMet = computed(() => - Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable)) - ); - - req.partialDisplay = jsx(() => ( + req.partialDisplay = amount => ( ) @@ -57,33 +55,81 @@ export function createCostRequirement( > {displayResource( req.resource, - unref(req.cost as ProcessedComputable) + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref( + req.spendResources as ProcessedComputable | undefined + ) ?? true + ) + : unref(req.cost as ProcessedComputable) )}{" "} {req.resource.displayName} - )); - req.display = jsx(() => ( + ); + req.display = amount => (
{unref(req.requiresPay as ProcessedComputable) ? "Costs: " : "Requires: "} {displayResource( req.resource, - unref(req.cost as ProcessedComputable) + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref( + req.spendResources as ProcessedComputable | undefined + ) ?? true + ) + : unref(req.cost as ProcessedComputable) )}{" "} {req.resource.displayName}
- )); + ); processComputable(req as T, "visibility"); setDefault(req, "visibility", Visibility.Visible); processComputable(req as T, "cost"); processComputable(req as T, "requiresPay"); + processComputable(req as T, "spendResources"); setDefault(req, "requiresPay", true); - setDefault(req, "pay", function () { - req.resource.value = Decimal.sub( - req.resource.value, - unref(req.cost as ProcessedComputable) - ).max(0); + setDefault(req, "pay", function (amount?: DecimalSource) { + const cost = + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref(req.spendResources as ProcessedComputable | undefined) ?? + true + ) + : unref(req.cost as ProcessedComputable); + 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 | 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) + ); + } + }); + } 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)) { - 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)) { requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible); if (requirements.length === 1) { @@ -136,7 +194,7 @@ export function displayRequirements(requirements: Requirements) {
Costs:{" "} {joinJSX( - withCosts.map(r => r.partialDisplay!()), + withCosts.map(r => r.partialDisplay!(amount)), <>, )}
@@ -145,7 +203,7 @@ export function displayRequirements(requirements: Requirements) {
Requires:{" "} {joinJSX( - withoutCosts.map(r => r.partialDisplay!()), + withoutCosts.map(r => r.partialDisplay!(amount)), <>, )}
@@ -156,10 +214,11 @@ export function displayRequirements(requirements: Requirements) { return requirements.display?.() ?? <>; } -export function payRequirements(requirements: Requirements) { +export function payRequirements(requirements: Requirements, buyMax = false) { + const amount = buyMax ? maxRequirementsMet(requirements) : 1; 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)) { - requirements.pay?.(); + requirements.pay?.(amount); } } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 217b980..2be632e 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,5 +1,6 @@ import { createResource, Resource } from "features/resources/resource"; import Formula, { + calculateCost, calculateMaxAffordable, GenericFormula, InvertibleFormula, @@ -983,45 +984,32 @@ describe("Buy Max", () => { }); describe("With spending", () => { test("Throws on formula with non-invertible integral", () => { - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.neg(10), - resource, - false - ); + const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); expect(() => maxAffordable.value).toThrow(); - expect(() => cost.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(10); - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.pow(1.05, variable), - resource, - false - ); + const formula = Formula.pow(1.05, variable); + const maxAffordable = calculateMaxAffordable(formula, resource, false); 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", () => { test("Throws on non-invertible formula", () => { - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.abs(10), - resource, - false - ); + const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false); expect(() => maxAffordable.value).toThrow(); - expect(() => cost.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(10); - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.pow(1.05, variable), - resource - ); + const formula = Formula.pow(1.05, variable); + const maxAffordable = calculateMaxAffordable(formula, resource); expect(maxAffordable.value).compare_tolerance(7); - expect(cost.value).compare_tolerance(7.35); + expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35); }); }); });