profectus-template/src/game/requirements.tsx

225 lines
8.6 KiB
TypeScript
Raw Normal View History

2022-12-31 20:57:09 +00:00
import { isArray } from "@vue/shared";
import { CoercableComponent, jsx, JSXFunction, 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";
2023-02-05 08:44:03 +00:00
import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from "./formulas";
2022-12-31 20:57:09 +00:00
/**
* 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}. */
2023-02-05 08:44:03 +00:00
partialDisplay?: (amount?: DecimalSource) => JSX.Element;
2022-12-31 20:57:09 +00:00
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
2023-02-05 08:44:03 +00:00
display?: (amount?: DecimalSource) => JSX.Element;
2022-12-31 20:57:09 +00:00
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
2023-02-05 08:44:03 +00:00
requirementMet: ProcessedComputable<DecimalSource | boolean>;
2022-12-31 20:57:09 +00:00
requiresPay: ProcessedComputable<boolean>;
2023-02-05 08:44:03 +00:00
buyMax?: ProcessedComputable<boolean>;
pay?: (amount?: DecimalSource) => void;
2022-12-31 20:57:09 +00:00
}
export type Requirements = Requirement | Requirement[];
export interface CostRequirementOptions {
resource: Resource;
2023-02-05 08:44:03 +00:00
cost: Computable<DecimalSource> | GenericFormula;
2022-12-31 20:57:09 +00:00
visibility?: Computable<Visibility.Visible | Visibility.None>;
2023-02-05 08:44:03 +00:00
requiresPay?: Computable<boolean>;
buyMax?: Computable<boolean>;
spendResources?: Computable<boolean>;
pay?: (amount?: DecimalSource) => void;
2022-12-31 20:57:09 +00:00
}
2023-02-05 08:44:03 +00:00
export function createCostRequirement<T extends CostRequirementOptions>(optionsFunc: () => T) {
2022-12-31 20:57:09 +00:00
return createLazyProxy(() => {
const req = optionsFunc() as T & Partial<Requirement>;
2023-02-05 08:44:03 +00:00
req.partialDisplay = amount => (
2022-12-31 20:57:09 +00:00
<span
style={
unref(req.requirementMet as ProcessedComputable<boolean>)
? ""
: "color: var(--danger)"
}
>
{displayResource(
req.resource,
2023-02-05 08:44:03 +00:00
req.cost instanceof Formula
? calculateCost(
req.cost,
amount ?? 1,
unref(
req.spendResources as ProcessedComputable<boolean> | undefined
) ?? true
)
: unref(req.cost as ProcessedComputable<DecimalSource>)
2022-12-31 20:57:09 +00:00
)}{" "}
{req.resource.displayName}
</span>
2023-02-05 08:44:03 +00:00
);
req.display = amount => (
2022-12-31 20:57:09 +00:00
<div>
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
{displayResource(
req.resource,
2023-02-05 08:44:03 +00:00
req.cost instanceof Formula
? calculateCost(
req.cost,
amount ?? 1,
unref(
req.spendResources as ProcessedComputable<boolean> | undefined
) ?? true
)
: unref(req.cost as ProcessedComputable<DecimalSource>)
2022-12-31 20:57:09 +00:00
)}{" "}
{req.resource.displayName}
</div>
2023-02-05 08:44:03 +00:00
);
2022-12-31 20:57:09 +00:00
processComputable(req as T, "visibility");
setDefault(req, "visibility", Visibility.Visible);
processComputable(req as T, "cost");
processComputable(req as T, "requiresPay");
2023-02-05 08:44:03 +00:00
processComputable(req as T, "spendResources");
2022-12-31 20:57:09 +00:00
setDefault(req, "requiresPay", true);
2023-02-05 08:44:03 +00:00
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);
2022-12-31 20:57:09 +00:00
});
2023-02-05 08:44:03 +00:00
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>)
);
}
});
}
2022-12-31 20:57:09 +00:00
return req as Requirement;
});
}
export function createVisibilityRequirement(feature: {
visibility: ProcessedComputable<Visibility>;
}): Requirement {
return createLazyProxy(() => ({
requirementMet: computed(() => unref(feature.visibility) === Visibility.Visible),
visibility: Visibility.None,
requiresPay: false
}));
}
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
}));
}
2023-02-05 08:44:03 +00:00
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);
}
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
2022-12-31 20:57:09 +00:00
if (isArray(requirements)) {
2023-02-05 08:44:03 +00:00
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
}
const reqsMet = unref(requirements.requirementMet);
if (typeof reqsMet === "boolean") {
return reqsMet ? Infinity : 0;
2022-12-31 20:57:09 +00:00
}
2023-02-05 08:44:03 +00:00
return reqsMet;
2022-12-31 20:57:09 +00:00
}
2023-02-05 08:44:03 +00:00
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
2022-12-31 20:57:09 +00:00
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(
2023-02-05 08:44:03 +00:00
withCosts.map(r => r.partialDisplay!(amount)),
2022-12-31 20:57:09 +00:00
<>, </>
)}
</div>
) : null}
{withoutCosts.length > 0 ? (
<div>
Requires:{" "}
{joinJSX(
2023-02-05 08:44:03 +00:00
withoutCosts.map(r => r.partialDisplay!(amount)),
2022-12-31 20:57:09 +00:00
<>, </>
)}
</div>
) : null}
</>
);
}
return requirements.display?.() ?? <></>;
}
2023-02-05 08:44:03 +00:00
export function payRequirements(requirements: Requirements, buyMax = false) {
const amount = buyMax ? maxRequirementsMet(requirements) : 1;
2022-12-31 20:57:09 +00:00
if (isArray(requirements)) {
2023-02-05 08:44:03 +00:00
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
2022-12-31 20:57:09 +00:00
} else if (unref(requirements.requiresPay)) {
2023-02-05 08:44:03 +00:00
requirements.pay?.(amount);
2022-12-31 20:57:09 +00:00
}
}