forked from profectus/Profectus
304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
import { isArray } from "@vue/shared";
|
|
import { CoercableComponent, jsx, setDefault, Visibility } from "features/feature";
|
|
import { displayResource, Resource } from "features/resources/resource";
|
|
import Decimal, { DecimalSource } from "lib/break_eternity";
|
|
import {
|
|
Computable,
|
|
convertComputable,
|
|
processComputable,
|
|
ProcessedComputable
|
|
} from "util/computed";
|
|
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.
|
|
* @see {@link createCostRequirement}
|
|
*/
|
|
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?: (amount?: DecimalSource) => JSX.Element;
|
|
/**
|
|
* The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}.
|
|
*/
|
|
display?: (amount?: DecimalSource) => JSX.Element;
|
|
/**
|
|
* Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property.
|
|
*/
|
|
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
|
|
/**
|
|
* Whether or not this requirement has been met.
|
|
*/
|
|
requirementMet: ProcessedComputable<DecimalSource | boolean>;
|
|
/**
|
|
* Whether or not this requirement will need to affect the game state when whatever is using this requirement gets triggered.
|
|
*/
|
|
requiresPay: ProcessedComputable<boolean>;
|
|
/**
|
|
* Whether or not this requirement can have multiple levels of requirements that can be completed at once.
|
|
*/
|
|
buyMax?: ProcessedComputable<boolean>;
|
|
/**
|
|
* Perform any effects to the game state that should happen when the requirement gets triggered.
|
|
* @param amount The amount of levels of requirements to pay for.
|
|
*/
|
|
pay?: (amount?: DecimalSource) => void;
|
|
}
|
|
|
|
/**
|
|
* Utility type for accepting 1 or more {@link Requirement}s.
|
|
*/
|
|
export type Requirements = Requirement | Requirement[];
|
|
|
|
/** An object that configures a {@link Requirement} based on a resource cost. */
|
|
export interface CostRequirementOptions {
|
|
/**
|
|
* The resource that will be checked for meeting the {@link cost}.
|
|
*/
|
|
resource: Resource;
|
|
/**
|
|
* The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case {@link buyMax} will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support buyMax by passing a custom {@link pay} function.
|
|
*/
|
|
cost: Computable<DecimalSource> | GenericFormula;
|
|
/**
|
|
* Pass-through to {@link Requirement.visibility}.
|
|
*/
|
|
visibility?: Computable<Visibility.Visible | Visibility.None>;
|
|
/**
|
|
* Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}.
|
|
*/
|
|
requiresPay?: Computable<boolean>;
|
|
/**
|
|
* Pass-through to {@link Requirement.buyMax}.
|
|
* @see {@link cost} for restrictions on buying max support.
|
|
*/
|
|
buyMax?: Computable<boolean>;
|
|
/**
|
|
* When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
|
|
* @see {Formula}
|
|
*/
|
|
spendResources?: Computable<boolean>;
|
|
/**
|
|
* Pass-through to {@link Requirement.pay}. May be required for buying max support.
|
|
* @see {@link cost} for restrictions on buying max support.
|
|
*/
|
|
pay?: (amount?: DecimalSource) => void;
|
|
}
|
|
|
|
/**
|
|
* Lazily creates a requirement with the given options, that is based on meeting an amount of a resource.
|
|
* @param optionsFunc Cost requirement options.
|
|
*/
|
|
export function createCostRequirement<T extends CostRequirementOptions>(
|
|
optionsFunc: () => T
|
|
): Requirement {
|
|
return createLazyProxy(() => {
|
|
const req = optionsFunc() as T & Partial<Requirement>;
|
|
|
|
req.partialDisplay = amount => (
|
|
<span
|
|
style={
|
|
unref(req.requirementMet as ProcessedComputable<boolean>)
|
|
? ""
|
|
: "color: var(--danger)"
|
|
}
|
|
>
|
|
{displayResource(
|
|
req.resource,
|
|
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}
|
|
</span>
|
|
);
|
|
req.display = amount => (
|
|
<div>
|
|
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
|
|
{displayResource(
|
|
req.resource,
|
|
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}
|
|
</div>
|
|
);
|
|
|
|
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 (amount?: DecimalSource) {
|
|
const cost =
|
|
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.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;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Utility function for creating a requirement that a specified vue feature is visible
|
|
* @param feature The feature to check the visibility of
|
|
*/
|
|
export function createVisibilityRequirement(feature: {
|
|
visibility: ProcessedComputable<Visibility>;
|
|
}): Requirement {
|
|
return createLazyProxy(() => ({
|
|
requirementMet: computed(() => unref(feature.visibility) === Visibility.Visible),
|
|
visibility: Visibility.None,
|
|
requiresPay: false
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Creates a requirement based on a true/false value
|
|
* @param requirement The boolean requirement to use
|
|
* @param display How to display this requirement to the user
|
|
*/
|
|
export function createBooleanRequirement(
|
|
requirement: Computable<boolean>,
|
|
display?: CoercableComponent
|
|
): Requirement {
|
|
return createLazyProxy(() => ({
|
|
requirementMet: convertComputable(requirement),
|
|
partialDisplay: display == null ? undefined : jsx(() => renderJSX(display)),
|
|
display: display == null ? undefined : jsx(() => <>Req: {renderJSX(display)}</>),
|
|
visibility: display == null ? Visibility.None : Visibility.Visible,
|
|
requiresPay: false
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Utility for checking if 1+ requirements are all met
|
|
* @param requirements The 1+ requirements to check
|
|
*/
|
|
export function requirementsMet(requirements: Requirements): boolean {
|
|
if (isArray(requirements)) {
|
|
return requirements.every(requirementsMet);
|
|
}
|
|
const reqsMet = unref(requirements.requirementMet);
|
|
return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0);
|
|
}
|
|
|
|
/**
|
|
* Calculates the maximum number of levels that could be acquired with the current requirement states. True/false requirements will be counted as Infinity or 0.
|
|
* @param requirements The 1+ requirements to check
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Utility function for display 1+ requirements compactly.
|
|
* @param requirements The 1+ requirements to display
|
|
* @param amount The amount of levels earned to be displayed
|
|
*/
|
|
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
|
if (isArray(requirements)) {
|
|
requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible);
|
|
if (requirements.length === 1) {
|
|
requirements = requirements[0];
|
|
}
|
|
}
|
|
if (isArray(requirements)) {
|
|
requirements = requirements.filter(r => "partialDisplay" in r);
|
|
const withCosts = requirements.filter(r => unref(r.requiresPay));
|
|
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
|
|
return (
|
|
<>
|
|
{withCosts.length > 0 ? (
|
|
<div>
|
|
Costs:{" "}
|
|
{joinJSX(
|
|
withCosts.map(r => r.partialDisplay!(amount)),
|
|
<>, </>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
{withoutCosts.length > 0 ? (
|
|
<div>
|
|
Requires:{" "}
|
|
{joinJSX(
|
|
withoutCosts.map(r => r.partialDisplay!(amount)),
|
|
<>, </>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
return requirements.display?.() ?? <></>;
|
|
}
|
|
|
|
/**
|
|
* Utility function for paying the costs for 1+ requirements
|
|
* @param requirements The 1+ requirements to pay
|
|
* @param amount How many levels to pay for
|
|
*/
|
|
export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
|
if (isArray(requirements)) {
|
|
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
|
} else if (unref(requirements.requiresPay)) {
|
|
requirements.pay?.(amount);
|
|
}
|
|
}
|