import { isArray } from "@vue/shared"; import ClickableComponent from "features/clickables/Clickable.vue"; import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; import { DefaultValue, Persistent, persistent } from "game/persistence"; import { createVisibilityRequirement, displayRequirements, maxRequirementsMet, payRequirements, Requirements, requirementsMet } from "game/requirements"; import type { DecimalSource } from "util/bignum"; import Decimal, { formatWhole } from "util/bignum"; import type { Computable, GetComputableType, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import { processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { coerceComponent, isCoercableComponent } from "util/vue"; import type { Ref } from "vue"; import { computed, unref } from "vue"; export const BuyableType = Symbol("Buyable"); export type BuyableDisplay = | CoercableComponent | { title?: CoercableComponent; description?: CoercableComponent; effectDisplay?: CoercableComponent; showAmount?: boolean; }; export interface BuyableOptions { visibility?: Computable; requirements: Requirements; purchaseLimit?: Computable; initialValue?: DecimalSource; classes?: Computable>; style?: Computable; mark?: Computable; small?: Computable; buyMax?: Computable; display?: Computable; onPurchase?: VoidFunction; } export interface BaseBuyable { id: string; amount: Persistent; maxed: Ref; canClick: ProcessedComputable; onClick: VoidFunction; purchase: VoidFunction; type: typeof BuyableType; [Component]: typeof ClickableComponent; [GatherProps]: () => Record; } export type Buyable = Replace< T & BaseBuyable, { visibility: GetComputableTypeWithDefault; requirements: GetComputableType; purchaseLimit: GetComputableTypeWithDefault; classes: GetComputableType; style: GetComputableType; mark: GetComputableType; small: GetComputableType; buyMax: GetComputableType; display: Ref; } >; export type GenericBuyable = Replace< Buyable, { visibility: ProcessedComputable; purchaseLimit: ProcessedComputable; } >; export function createBuyable( optionsFunc: OptionsFunc ): Buyable { const amount = persistent(0); return createLazyProxy(() => { const buyable = optionsFunc(); buyable.id = getUniqueID("buyable-"); buyable.type = BuyableType; buyable[Component] = ClickableComponent; buyable.amount = amount; buyable.amount[DefaultValue] = buyable.initialValue ?? 0; const limitRequirement = { requirementMet: computed(() => Decimal.sub( unref((buyable as GenericBuyable).purchaseLimit), (buyable as GenericBuyable).amount.value ) ), 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, unref((buyable as GenericBuyable).purchaseLimit) ) ); processComputable(buyable as T, "classes"); const classes = buyable.classes as ProcessedComputable> | undefined; buyable.classes = computed(() => { const currClasses = unref(classes) || {}; if ((buyable as GenericBuyable).maxed.value) { currClasses.bought = true; } return currClasses; }); 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.canClick)) { return; } payRequirements( buyable.requirements, unref(genericBuyable.buyMax) ? maxRequirementsMet(genericBuyable.requirements) : 1 ); genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); genericBuyable.onPurchase?.(); }; processComputable(buyable as T, "display"); const display = buyable.display; buyable.display = jsx(() => { // TODO once processComputable types correctly, remove this "as X" const currDisplay = unref(display) as BuyableDisplay; if (isCoercableComponent(currDisplay)) { const CurrDisplay = coerceComponent(currDisplay); return ; } if (currDisplay != null) { const genericBuyable = buyable as GenericBuyable; const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Description = coerceComponent(currDisplay.description ?? ""); const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? ""); return ( {currDisplay.title == null ? null : (
</div> )} {currDisplay.description == null ? null : <Description />} {currDisplay.showAmount === false ? null : ( <div> <br /> {unref(genericBuyable.purchaseLimit) === Decimal.dInf ? ( <>Amount: {formatWhole(genericBuyable.amount.value)}</> ) : ( <> Amount: {formatWhole(genericBuyable.amount.value)} /{" "} {formatWhole(unref(genericBuyable.purchaseLimit))} </> )} </div> )} {currDisplay.effectDisplay == null ? null : ( <div> <br /> Currently: <EffectDisplay /> </div> )} {genericBuyable.maxed.value ? null : ( <div> <br /> {displayRequirements( genericBuyable.requirements, unref(genericBuyable.buyMax) ? maxRequirementsMet(genericBuyable.requirements) : 1 )} </div> )} </span> ); } return ""; }); processComputable(buyable as T, "visibility"); setDefault(buyable, "visibility", Visibility.Visible); processComputable(buyable as T, "purchaseLimit"); setDefault(buyable, "purchaseLimit", Decimal.dInf); processComputable(buyable as T, "style"); processComputable(buyable as T, "mark"); processComputable(buyable as T, "small"); processComputable(buyable as T, "buyMax"); buyable[GatherProps] = function (this: GenericBuyable) { const { display, visibility, style, classes, onClick, canClick, small, mark, id } = this; return { display, visibility, style: unref(style), classes, onClick, canClick, small, mark, id }; }; return buyable as unknown as Buyable<T>; }); }