Implemented requirements system

This commit is contained in:
thepaperpilot 2022-12-31 14:57:09 -06:00
parent 0728567cb9
commit 011df9cf2e
5 changed files with 235 additions and 135 deletions

View file

@ -1,3 +1,4 @@
import { isArray } from "@vue/shared";
import ClickableComponent from "features/clickables/Clickable.vue";
import type {
CoercableComponent,
@ -7,11 +8,16 @@ import type {
StyleValue
} from "features/feature";
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
import type { Resource } from "features/resources/resource";
import { DefaultValue, Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import { DefaultValue, Persistent, persistent } from "game/persistence";
import {
createVisibilityRequirement,
displayRequirements,
payRequirements,
Requirements,
requirementsMet
} from "game/requirements";
import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatWhole } from "util/bignum";
import Decimal, { formatWhole } from "util/bignum";
import type {
Computable,
GetComputableType,
@ -21,8 +27,7 @@ import type {
import { processComputable } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { coerceComponent, isCoercableComponent } from "util/vue";
import { isReadonly, Ref } from "vue";
import { computed, unref } from "vue";
import { computed, Ref, unref } from "vue";
export const BuyableType = Symbol("Buyable");
@ -37,9 +42,7 @@ export type BuyableDisplay =
export interface BuyableOptions {
visibility?: Computable<Visibility>;
cost?: Computable<DecimalSource>;
resource?: Resource;
canPurchase?: Computable<boolean>;
requirements: Requirements;
purchaseLimit?: Computable<DecimalSource>;
initialValue?: DecimalSource;
classes?: Computable<Record<string, boolean>>;
@ -47,14 +50,13 @@ export interface BuyableOptions {
mark?: Computable<boolean | string>;
small?: Computable<boolean>;
display?: Computable<BuyableDisplay>;
onPurchase?: (cost: DecimalSource | undefined) => void;
onPurchase?: VoidFunction;
}
export interface BaseBuyable {
id: string;
amount: Persistent<DecimalSource>;
maxed: Ref<boolean>;
canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>;
onClick: VoidFunction;
purchase: VoidFunction;
@ -67,9 +69,7 @@ export type Buyable<T extends BuyableOptions> = Replace<
T & BaseBuyable,
{
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
cost: GetComputableType<T["cost"]>;
resource: GetComputableType<T["resource"]>;
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
requirements: GetComputableType<T["requirements"]>;
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>;
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
@ -83,7 +83,6 @@ export type GenericBuyable = Replace<
Buyable<BuyableOptions>,
{
visibility: ProcessedComputable<Visibility>;
canPurchase: ProcessedComputable<boolean>;
purchaseLimit: ProcessedComputable<DecimalSource>;
}
>;
@ -95,40 +94,31 @@ export function createBuyable<T extends BuyableOptions>(
return createLazyProxy(() => {
const buyable = optionsFunc();
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn(
"Cannot create buyable without a canPurchase property or a resource and cost property",
buyable
);
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
}
buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType;
buyable[Component] = ClickableComponent as GenericComponent;
buyable.amount = amount;
buyable.amount[DefaultValue] = buyable.initialValue ?? 0;
buyable.canAfford = computed(() => {
const genericBuyable = buyable as GenericBuyable;
const cost = unref(genericBuyable.cost);
return (
genericBuyable.resource != null &&
cost != null &&
Decimal.gte(genericBuyable.resource.value, cost)
);
});
if (buyable.canPurchase == null) {
buyable.canPurchase = computed(
() =>
unref((buyable as GenericBuyable).visibility) === Visibility.Visible &&
unref((buyable as GenericBuyable).canAfford) &&
Decimal.lt(
(buyable as GenericBuyable).amount.value,
unref((buyable as GenericBuyable).purchaseLimit)
)
);
const limitRequirement = {
requirementMet: computed(() =>
Decimal.lt(
(buyable as GenericBuyable).amount.value,
unref((buyable as GenericBuyable).purchaseLimit)
)
),
requiresPay: false,
visibility: Visibility.None
} as const;
const visibilityRequirement = createVisibilityRequirement(buyable as GenericBuyable);
if (isArray(buyable.requirements)) {
buyable.requirements.unshift(visibilityRequirement);
buyable.requirements.push(limitRequirement);
} else {
buyable.requirements = [visibilityRequirement, buyable.requirements, limitRequirement];
}
buyable.maxed = computed(() =>
Decimal.gte(
(buyable as GenericBuyable).amount.value,
@ -144,30 +134,18 @@ export function createBuyable<T extends BuyableOptions>(
}
return currClasses;
});
processComputable(buyable as T, "canPurchase");
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
buyable.canClick = computed(() => requirementsMet(buyable.requirements));
buyable.onClick = buyable.purchase =
buyable.onClick ??
buyable.purchase ??
function (this: GenericBuyable) {
const genericBuyable = buyable as GenericBuyable;
if (!unref(genericBuyable.canPurchase)) {
if (!unref(genericBuyable.canClick)) {
return;
}
const cost = unref(genericBuyable.cost);
if (
genericBuyable.cost != null &&
genericBuyable.resource != null &&
!isReadonly(genericBuyable.resource)
) {
genericBuyable.resource.value = Decimal.sub(
genericBuyable.resource.value,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
cost!
);
}
payRequirements(buyable.requirements);
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
genericBuyable.onPurchase?.(cost);
genericBuyable.onPurchase?.();
};
processComputable(buyable as T, "display");
const display = buyable.display;
@ -178,7 +156,7 @@ export function createBuyable<T extends BuyableOptions>(
const CurrDisplay = coerceComponent(currDisplay);
return <CurrDisplay />;
}
if (currDisplay != null && buyable.cost != null && buyable.resource != null) {
if (currDisplay != null) {
const genericBuyable = buyable as GenericBuyable;
const Title = coerceComponent(currDisplay.title ?? "", "h3");
const Description = coerceComponent(currDisplay.description ?? "");
@ -211,12 +189,12 @@ export function createBuyable<T extends BuyableOptions>(
Currently: <EffectDisplay />
</div>
)}
{genericBuyable.cost != null && !genericBuyable.maxed.value ? (
{genericBuyable.maxed.value ? null : (
<div>
Cost: {format(unref(genericBuyable.cost))}{" "}
{buyable.resource.displayName}
<br />
{displayRequirements(genericBuyable.requirements)}
</div>
) : null}
)}
</span>
);
}
@ -225,8 +203,6 @@ export function createBuyable<T extends BuyableOptions>(
processComputable(buyable as T, "visibility");
setDefault(buyable, "visibility", Visibility.Visible);
processComputable(buyable as T, "cost");
processComputable(buyable as T, "resource");
processComputable(buyable as T, "purchaseLimit");
setDefault(buyable, "purchaseLimit", Decimal.dInf);
processComputable(buyable as T, "style");

View file

@ -30,10 +30,8 @@ import MarkNode from "components/MarkNode.vue";
import Node from "components/Node.vue";
import type { StyleValue } from "features/feature";
import { jsx, Visibility } from "features/feature";
import type { Resource } from "features/resources/resource";
import { displayResource } from "features/resources/resource";
import type { GenericUpgrade } from "features/upgrades/upgrade";
import type { DecimalSource } from "util/bignum";
import { displayRequirements, Requirements } from "game/requirements";
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
import type { Component, PropType, UnwrapRef } from "vue";
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
@ -50,8 +48,10 @@ export default defineComponent({
},
style: processedPropType<StyleValue>(String, Object, Array),
classes: processedPropType<Record<string, boolean>>(Object),
resource: Object as PropType<Resource>,
cost: processedPropType<DecimalSource>(String, Object, Number),
requirements: {
type: Object as PropType<Requirements>,
required: true
},
canPurchase: {
type: processedPropType<boolean>(Boolean),
required: true
@ -75,7 +75,7 @@ export default defineComponent({
MarkNode
},
setup(props) {
const { display, cost } = toRefs(props);
const { display, requirements, bought } = toRefs(props);
const component = shallowRef<Component | string>("");
@ -89,7 +89,6 @@ export default defineComponent({
component.value = coerceComponent(currDisplay);
return;
}
const currCost = unwrapRef(cost);
const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description, "div");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
@ -107,14 +106,7 @@ export default defineComponent({
Currently: <EffectDisplay />
</div>
) : null}
{props.resource != null && currDisplay.showCost !== false ? (
<>
<br />
Cost: {props.resource &&
displayResource(props.resource, currCost)}{" "}
{props.resource?.displayName}
</>
) : null}
{bought.value ? null : <><br />{displayRequirements(requirements.value)}</>}
</span>
))
);

View file

@ -1,3 +1,4 @@
import { isArray } from "@vue/shared";
import type {
CoercableComponent,
GenericComponent,
@ -13,13 +14,16 @@ import {
setDefault,
Visibility
} from "features/feature";
import type { Resource } from "features/resources/resource";
import UpgradeComponent from "features/upgrades/Upgrade.vue";
import type { GenericLayer } from "game/layers";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import type { DecimalSource } from "util/bignum";
import Decimal from "util/bignum";
import {
createVisibilityRequirement,
payRequirements,
Requirements,
requirementsMet
} from "game/requirements";
import { isFunction } from "util/common";
import type {
Computable,
@ -29,8 +33,7 @@ import type {
} from "util/computed";
import { processComputable } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { isReadonly, Ref } from "vue";
import { computed, unref } from "vue";
import { computed, Ref, unref } from "vue";
export const UpgradeType = Symbol("Upgrade");
@ -44,13 +47,10 @@ export interface UpgradeOptions {
title?: CoercableComponent;
description: CoercableComponent;
effectDisplay?: CoercableComponent;
showCost?: boolean;
}
>;
requirements: Requirements;
mark?: Computable<boolean | string>;
cost?: Computable<DecimalSource>;
resource?: Resource;
canAfford?: Computable<boolean>;
onPurchase?: VoidFunction;
}
@ -71,9 +71,8 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
display: GetComputableType<T["display"]>;
requirements: GetComputableType<T["requirements"]>;
mark: GetComputableType<T["mark"]>;
cost: GetComputableType<T["cost"]>;
canAfford: GetComputableTypeWithDefault<T["canAfford"], Ref<boolean>>;
}
>;
@ -81,7 +80,6 @@ export type GenericUpgrade = Replace<
Upgrade<UpgradeOptions>,
{
visibility: ProcessedComputable<Visibility>;
canPurchase: ProcessedComputable<boolean>;
}
>;
@ -95,59 +93,31 @@ export function createUpgrade<T extends UpgradeOptions>(
upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent as GenericComponent;
if (upgrade.canAfford == null && (upgrade.resource == null || upgrade.cost == null)) {
console.warn(
"Error: can't create upgrade without a canAfford property or a resource and cost property",
upgrade
);
}
upgrade.bought = bought;
if (upgrade.canAfford == null) {
upgrade.canAfford = computed(() => {
const genericUpgrade = upgrade as GenericUpgrade;
return (
genericUpgrade.resource != null &&
genericUpgrade.cost != null &&
Decimal.gte(genericUpgrade.resource.value, unref(genericUpgrade.cost))
);
});
} else {
processComputable(upgrade as T, "canAfford");
}
upgrade.canPurchase = computed(
() =>
unref((upgrade as GenericUpgrade).visibility) === Visibility.Visible &&
unref((upgrade as GenericUpgrade).canAfford) &&
!unref(upgrade.bought)
);
upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
upgrade.purchase = function () {
const genericUpgrade = upgrade as GenericUpgrade;
if (!unref(genericUpgrade.canPurchase)) {
return;
}
if (
genericUpgrade.resource != null &&
!isReadonly(genericUpgrade.resource) &&
genericUpgrade.cost != null
) {
genericUpgrade.resource.value = Decimal.sub(
genericUpgrade.resource.value,
unref(genericUpgrade.cost)
);
}
payRequirements(upgrade.requirements);
bought.value = true;
genericUpgrade.onPurchase?.();
};
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
if (isArray(upgrade.requirements)) {
upgrade.requirements.unshift(visibilityRequirement);
} else {
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
}
processComputable(upgrade as T, "visibility");
setDefault(upgrade, "visibility", Visibility.Visible);
processComputable(upgrade as T, "classes");
processComputable(upgrade as T, "style");
processComputable(upgrade as T, "display");
processComputable(upgrade as T, "mark");
processComputable(upgrade as T, "cost");
processComputable(upgrade as T, "resource");
upgrade[GatherProps] = function (this: GenericUpgrade) {
const {
@ -155,8 +125,7 @@ export function createUpgrade<T extends UpgradeOptions>(
visibility,
style,
classes,
resource,
cost,
requirements,
canPurchase,
bought,
mark,
@ -168,8 +137,7 @@ export function createUpgrade<T extends UpgradeOptions>(
visibility,
style: unref(style),
classes,
resource,
cost,
requirements,
canPurchase,
bought,
mark,

View file

@ -15,8 +15,7 @@ import { computed, unref } from "vue";
* An object that can be used to apply or unapply some modification to a number.
* Being reversible requires the operation being invertible, but some features may rely on that.
* Descriptions can be optionally included for displaying them to the player.
* The built-in modifier creators are designed to display the modifiers using.
* {@link createModifierSection}.
* The built-in modifier creators are designed to display the modifiers using {@link createModifierSection}.
*/
export interface Modifier {
/** Applies some operation on the input and returns the result. */

165
src/game/requirements.tsx Normal file
View file

@ -0,0 +1,165 @@
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";
/**
* 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?: JSXFunction;
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
display?: JSXFunction;
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
requirementMet: ProcessedComputable<boolean>;
requiresPay: ProcessedComputable<boolean>;
pay?: VoidFunction;
}
export type Requirements = Requirement | Requirement[];
export interface CostRequirementOptions {
resource: Resource;
cost: Computable<DecimalSource>;
visibility?: Computable<Visibility.Visible | Visibility.None>;
requiresPay?: ProcessedComputable<boolean>;
pay?: VoidFunction;
}
export function createCostRequirement<T extends CostRequirementOptions>(
optionsFunc: () => T
): Requirement {
return createLazyProxy(() => {
const req = optionsFunc() as T & Partial<Requirement>;
req.requirementMet = computed(() =>
Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable<DecimalSource>))
);
req.partialDisplay = jsx(() => (
<span
style={
unref(req.requirementMet as ProcessedComputable<boolean>)
? ""
: "color: var(--danger)"
}
>
{displayResource(
req.resource,
unref(req.cost as ProcessedComputable<DecimalSource>)
)}{" "}
{req.resource.displayName}
</span>
));
req.display = jsx(() => (
<div>
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
{displayResource(
req.resource,
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");
setDefault(req, "requiresPay", true);
setDefault(req, "pay", function () {
req.resource.value = Decimal.sub(
req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>)
).max(0);
});
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
}));
}
export function requirementsMet(requirements: Requirements) {
if (isArray(requirements)) {
return requirements.every(r => unref(r.requirementMet));
}
return unref(requirements.requirementMet);
}
export function displayRequirements(requirements: Requirements) {
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!()),
<>, </>
)}
</div>
) : null}
{withoutCosts.length > 0 ? (
<div>
Requires:{" "}
{joinJSX(
withoutCosts.map(r => r.partialDisplay!()),
<>, </>
)}
</div>
) : null}
</>
);
}
return requirements.display?.() ?? <></>;
}
export function payRequirements(requirements: Requirements) {
if (isArray(requirements)) {
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.());
} else if (unref(requirements.requiresPay)) {
requirements.pay?.();
}
}