From 3a4b15bd8f2f02f6b0b4dfdfaf1ed4a91b041163 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sat, 31 Dec 2022 14:57:09 -0600 Subject: [PATCH] Implemented requirements system --- src/features/buyable.tsx | 108 ++++++++----------- src/features/upgrades/Upgrade.vue | 22 ++-- src/features/upgrades/upgrade.ts | 74 +++++--------- src/game/modifiers.tsx | 3 +- src/game/requirements.tsx | 165 ++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 131 deletions(-) create mode 100644 src/game/requirements.tsx diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx index 30edd29..cdff9a0 100644 --- a/src/features/buyable.tsx +++ b/src/features/buyable.tsx @@ -1,17 +1,17 @@ +import { isArray } from "@vue/shared"; import ClickableComponent from "features/clickables/Clickable.vue"; -import type { - CoercableComponent, - GenericComponent, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; +import type { CoercableComponent, OptionsFunc, Replace, 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, @@ -37,9 +37,7 @@ export type BuyableDisplay = export interface BuyableOptions { visibility?: Computable; - cost?: Computable; - resource?: Resource; - canPurchase?: Computable; + requirements: Requirements; purchaseLimit?: Computable; initialValue?: DecimalSource; classes?: Computable>; @@ -47,14 +45,13 @@ export interface BuyableOptions { mark?: Computable; small?: Computable; display?: Computable; - onPurchase?: (cost: DecimalSource | undefined) => void; + onPurchase?: VoidFunction; } export interface BaseBuyable { id: string; amount: Persistent; maxed: Ref; - canAfford: Ref; canClick: ProcessedComputable; onClick: VoidFunction; purchase: VoidFunction; @@ -67,9 +64,7 @@ export type Buyable = Replace< T & BaseBuyable, { visibility: GetComputableTypeWithDefault; - cost: GetComputableType; - resource: GetComputableType; - canPurchase: GetComputableTypeWithDefault>; + requirements: GetComputableType; purchaseLimit: GetComputableTypeWithDefault; classes: GetComputableType; style: GetComputableType; @@ -83,7 +78,6 @@ export type GenericBuyable = Replace< Buyable, { visibility: ProcessedComputable; - canPurchase: ProcessedComputable; purchaseLimit: ProcessedComputable; } >; @@ -95,40 +89,31 @@ export function createBuyable( 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; 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,26 +129,18 @@ export function createBuyable( } return currClasses; }); - processComputable(buyable as T, "canPurchase"); - buyable.canClick = buyable.canPurchase as ProcessedComputable; + 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) { - genericBuyable.resource.value = Decimal.sub( - genericBuyable.resource.value, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - cost! - ); - genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); - } - genericBuyable.onPurchase?.(cost); + payRequirements(buyable.requirements); + genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); + genericBuyable.onPurchase?.(); }; processComputable(buyable as T, "display"); const display = buyable.display; @@ -174,7 +151,7 @@ export function createBuyable( const CurrDisplay = coerceComponent(currDisplay); return ; } - 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 ?? ""); @@ -207,13 +184,12 @@ export function createBuyable( Currently: )} - {genericBuyable.cost != null && !genericBuyable.maxed.value ? ( + {genericBuyable.maxed.value ? null : (

- Cost: {format(unref(genericBuyable.cost))}{" "} - {buyable.resource.displayName} + {displayRequirements(genericBuyable.requirements)}
- ) : null} + )} ); } @@ -222,8 +198,6 @@ export function createBuyable( 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"); diff --git a/src/features/upgrades/Upgrade.vue b/src/features/upgrades/Upgrade.vue index 3f6903c..7c45d75 100644 --- a/src/features/upgrades/Upgrade.vue +++ b/src/features/upgrades/Upgrade.vue @@ -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(String, Object, Array), classes: processedPropType>(Object), - resource: Object as PropType, - cost: processedPropType(String, Object, Number), + requirements: { + type: Object as PropType, + required: true + }, canPurchase: { type: processedPropType(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(""); @@ -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: ) : null} - {props.resource != null ? ( - <> -
- Cost: {props.resource && - displayResource(props.resource, currCost)}{" "} - {props.resource?.displayName} - - ) : null} + {bought.value ? null : <>
{displayRequirements(requirements.value)}} )) ); diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 3235e45..656d891 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -1,4 +1,11 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import { isArray } from "@vue/shared"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, findFeatures, @@ -7,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, @@ -40,10 +50,8 @@ export interface UpgradeOptions { effectDisplay?: CoercableComponent; } >; + requirements: Requirements; mark?: Computable; - cost?: Computable; - resource?: Resource; - canAfford?: Computable; onPurchase?: VoidFunction; } @@ -64,9 +72,8 @@ export type Upgrade = Replace< classes: GetComputableType; style: GetComputableType; display: GetComputableType; + requirements: GetComputableType; mark: GetComputableType; - cost: GetComputableType; - canAfford: GetComputableTypeWithDefault>; } >; @@ -74,7 +81,6 @@ export type GenericUpgrade = Replace< Upgrade, { visibility: ProcessedComputable; - canPurchase: ProcessedComputable; } >; @@ -88,55 +94,31 @@ export function createUpgrade( upgrade.type = UpgradeType; upgrade[Component] = UpgradeComponent; - 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 && 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 { @@ -144,8 +126,7 @@ export function createUpgrade( visibility, style, classes, - resource, - cost, + requirements, canPurchase, bought, mark, @@ -157,8 +138,7 @@ export function createUpgrade( visibility, style: unref(style), classes, - resource, - cost, + requirements, canPurchase, bought, mark, diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index 851423e..830e937 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -14,8 +14,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. */ diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx new file mode 100644 index 0000000..607800b --- /dev/null +++ b/src/game/requirements.tsx @@ -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; + requirementMet: ProcessedComputable; + requiresPay: ProcessedComputable; + pay?: VoidFunction; +} + +export type Requirements = Requirement | Requirement[]; + +export interface CostRequirementOptions { + resource: Resource; + cost: Computable; + visibility?: Computable; + requiresPay?: ProcessedComputable; + pay?: VoidFunction; +} + +export function createCostRequirement( + optionsFunc: () => T +): Requirement { + return createLazyProxy(() => { + const req = optionsFunc() as T & Partial; + + req.requirementMet = computed(() => + Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable)) + ); + + req.partialDisplay = jsx(() => ( + ) + ? "" + : "color: var(--danger)" + } + > + {displayResource( + req.resource, + unref(req.cost as ProcessedComputable) + )}{" "} + {req.resource.displayName} + + )); + req.display = jsx(() => ( +
+ {unref(req.requiresPay as ProcessedComputable) ? "Costs: " : "Requires: "} + {displayResource( + req.resource, + 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"); + setDefault(req, "requiresPay", true); + setDefault(req, "pay", function () { + req.resource.value = Decimal.sub( + req.resource.value, + unref(req.cost as ProcessedComputable) + ).max(0); + }); + + return req as Requirement; + }); +} + +export function createVisibilityRequirement(feature: { + visibility: ProcessedComputable; +}): Requirement { + return createLazyProxy(() => ({ + requirementMet: computed(() => unref(feature.visibility) === Visibility.Visible), + visibility: Visibility.None, + requiresPay: false + })); +} + +export function createBooleanRequirement( + requirement: Computable, + 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 ? ( +
+ Costs:{" "} + {joinJSX( + withCosts.map(r => r.partialDisplay!()), + <>, + )} +
+ ) : null} + {withoutCosts.length > 0 ? ( +
+ Requires:{" "} + {joinJSX( + withoutCosts.map(r => r.partialDisplay!()), + <>, + )} +
+ ) : 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?.(); + } +}