2022-03-04 03:39:48 +00:00
|
|
|
import ClickableComponent from "features/clickables/Clickable.vue";
|
|
|
|
import { Resource } from "features/resources/resource";
|
2022-04-23 23:20:15 +00:00
|
|
|
import { Persistent, persistent } from "game/persistence";
|
2022-03-04 03:39:48 +00:00
|
|
|
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
|
2022-01-14 04:25:47 +00:00
|
|
|
import {
|
|
|
|
Computable,
|
|
|
|
GetComputableType,
|
|
|
|
GetComputableTypeWithDefault,
|
|
|
|
processComputable,
|
|
|
|
ProcessedComputable
|
2022-03-04 03:39:48 +00:00
|
|
|
} from "util/computed";
|
|
|
|
import { createLazyProxy } from "util/proxies";
|
|
|
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
2022-01-14 04:25:47 +00:00
|
|
|
import { computed, Ref, unref } from "vue";
|
|
|
|
import {
|
|
|
|
CoercableComponent,
|
|
|
|
Component,
|
2022-04-11 00:04:56 +00:00
|
|
|
OptionsFunc,
|
2022-02-27 19:49:34 +00:00
|
|
|
GatherProps,
|
2022-01-14 04:25:47 +00:00
|
|
|
getUniqueID,
|
2022-02-27 19:49:34 +00:00
|
|
|
jsx,
|
2022-01-14 04:25:47 +00:00
|
|
|
Replace,
|
|
|
|
setDefault,
|
|
|
|
StyleValue,
|
|
|
|
Visibility
|
|
|
|
} from "./feature";
|
|
|
|
|
|
|
|
export const BuyableType = Symbol("Buyable");
|
|
|
|
|
2022-03-09 01:40:51 +00:00
|
|
|
export type BuyableDisplay =
|
2022-01-14 04:25:47 +00:00
|
|
|
| CoercableComponent
|
|
|
|
| {
|
|
|
|
title?: CoercableComponent;
|
|
|
|
description: CoercableComponent;
|
|
|
|
effectDisplay?: CoercableComponent;
|
|
|
|
};
|
|
|
|
|
|
|
|
export interface BuyableOptions {
|
|
|
|
visibility?: Computable<Visibility>;
|
|
|
|
cost?: Computable<DecimalSource>;
|
2022-01-28 04:47:26 +00:00
|
|
|
resource?: Resource;
|
2022-01-14 04:25:47 +00:00
|
|
|
canPurchase?: Computable<boolean>;
|
|
|
|
purchaseLimit?: Computable<DecimalSource>;
|
|
|
|
classes?: Computable<Record<string, boolean>>;
|
|
|
|
style?: Computable<StyleValue>;
|
|
|
|
mark?: Computable<boolean | string>;
|
|
|
|
small?: Computable<boolean>;
|
|
|
|
display?: Computable<BuyableDisplay>;
|
|
|
|
onPurchase?: (cost: DecimalSource) => void;
|
|
|
|
}
|
|
|
|
|
2022-04-23 23:20:15 +00:00
|
|
|
export interface BaseBuyable {
|
2022-01-14 04:25:47 +00:00
|
|
|
id: string;
|
2022-04-23 23:20:15 +00:00
|
|
|
amount: Persistent<DecimalSource>;
|
2022-02-27 19:49:34 +00:00
|
|
|
maxed: Ref<boolean>;
|
2022-01-14 04:25:47 +00:00
|
|
|
canAfford: Ref<boolean>;
|
|
|
|
canClick: ProcessedComputable<boolean>;
|
|
|
|
onClick: VoidFunction;
|
|
|
|
purchase: VoidFunction;
|
|
|
|
type: typeof BuyableType;
|
|
|
|
[Component]: typeof ClickableComponent;
|
2022-02-27 19:49:34 +00:00
|
|
|
[GatherProps]: () => Record<string, unknown>;
|
2022-01-14 04:25:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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>>;
|
2022-03-11 15:16:19 +00:00
|
|
|
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>;
|
2022-01-14 04:25:47 +00:00
|
|
|
classes: GetComputableType<T["classes"]>;
|
|
|
|
style: GetComputableType<T["style"]>;
|
|
|
|
mark: GetComputableType<T["mark"]>;
|
|
|
|
small: GetComputableType<T["small"]>;
|
|
|
|
display: Ref<CoercableComponent>;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
|
|
|
|
export type GenericBuyable = Replace<
|
|
|
|
Buyable<BuyableOptions>,
|
|
|
|
{
|
|
|
|
visibility: ProcessedComputable<Visibility>;
|
|
|
|
canPurchase: ProcessedComputable<boolean>;
|
|
|
|
purchaseLimit: ProcessedComputable<DecimalSource>;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
|
|
|
|
export function createBuyable<T extends BuyableOptions>(
|
2022-04-11 00:04:56 +00:00
|
|
|
optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable>
|
2022-01-14 04:25:47 +00:00
|
|
|
): Buyable<T> {
|
2022-04-23 23:20:15 +00:00
|
|
|
const amount = persistent<DecimalSource>(0);
|
|
|
|
return createLazyProxy(() => {
|
|
|
|
const buyable = optionsFunc();
|
2022-01-14 04:25:47 +00:00
|
|
|
|
2022-02-27 19:49:34 +00:00
|
|
|
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";
|
2022-01-14 04:25:47 +00:00
|
|
|
}
|
2022-02-27 19:49:34 +00:00
|
|
|
|
|
|
|
buyable.id = getUniqueID("buyable-");
|
|
|
|
buyable.type = BuyableType;
|
|
|
|
buyable[Component] = ClickableComponent;
|
|
|
|
|
2022-04-23 23:20:15 +00:00
|
|
|
buyable.amount = amount;
|
2022-02-27 19:49:34 +00:00
|
|
|
buyable.canAfford = computed(() => {
|
|
|
|
const genericBuyable = buyable as GenericBuyable;
|
|
|
|
const cost = unref(genericBuyable.cost);
|
2022-01-14 04:25:47 +00:00
|
|
|
return (
|
2022-02-27 19:49:34 +00:00
|
|
|
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)
|
|
|
|
)
|
2022-01-14 04:25:47 +00:00
|
|
|
);
|
|
|
|
}
|
2022-02-27 19:49:34 +00:00
|
|
|
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<Record<string, boolean>> | undefined;
|
|
|
|
buyable.classes = computed(() => {
|
|
|
|
const currClasses = unref(classes) || {};
|
|
|
|
if ((buyable as GenericBuyable).maxed.value) {
|
|
|
|
currClasses.bought = true;
|
|
|
|
}
|
|
|
|
return currClasses;
|
|
|
|
});
|
|
|
|
processComputable(buyable as T, "canPurchase");
|
|
|
|
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
|
|
|
|
buyable.onClick = buyable.purchase = function () {
|
|
|
|
const genericBuyable = buyable as GenericBuyable;
|
|
|
|
if (
|
|
|
|
!unref(genericBuyable.canPurchase) ||
|
|
|
|
genericBuyable.cost == null ||
|
|
|
|
genericBuyable.resource == null
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const cost = unref(genericBuyable.cost);
|
|
|
|
genericBuyable.resource.value = Decimal.sub(genericBuyable.resource.value, cost);
|
|
|
|
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
|
|
|
|
this.onPurchase?.(cost);
|
|
|
|
};
|
|
|
|
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;
|
2022-03-16 16:20:53 +00:00
|
|
|
if (isCoercableComponent(currDisplay)) {
|
2022-03-16 16:24:54 +00:00
|
|
|
const CurrDisplay = coerceComponent(currDisplay);
|
|
|
|
return <CurrDisplay />;
|
2022-03-16 16:20:53 +00:00
|
|
|
}
|
|
|
|
if (currDisplay != null && buyable.cost != null && buyable.resource != null) {
|
2022-02-27 19:49:34 +00:00
|
|
|
const genericBuyable = buyable as GenericBuyable;
|
|
|
|
const Title = coerceComponent(currDisplay.title || "", "h3");
|
|
|
|
const Description = coerceComponent(currDisplay.description);
|
|
|
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
2022-03-11 15:16:19 +00:00
|
|
|
const amountDisplay =
|
|
|
|
unref(genericBuyable.purchaseLimit) === Decimal.dInf ? (
|
|
|
|
<>Amount: {formatWhole(genericBuyable.amount.value)}</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
|
|
|
|
{formatWhole(unref(genericBuyable.purchaseLimit))}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
2022-02-27 19:49:34 +00:00
|
|
|
return (
|
|
|
|
<span>
|
|
|
|
{currDisplay.title ? (
|
|
|
|
<div>
|
|
|
|
<Title />
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
<Description />
|
|
|
|
<div>
|
|
|
|
<br />
|
2022-03-11 15:16:19 +00:00
|
|
|
{amountDisplay}
|
2022-02-27 19:49:34 +00:00
|
|
|
</div>
|
|
|
|
{currDisplay.effectDisplay ? (
|
|
|
|
<div>
|
|
|
|
<br />
|
|
|
|
Currently: <EffectDisplay />
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{genericBuyable.cost && !genericBuyable.maxed.value ? (
|
|
|
|
<div>
|
|
|
|
<br />
|
|
|
|
Cost: {format(unref(genericBuyable.cost) || 0)}{" "}
|
|
|
|
{buyable.resource.displayName}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
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");
|
2022-03-11 15:16:19 +00:00
|
|
|
setDefault(buyable, "purchaseLimit", Decimal.dInf);
|
2022-02-27 19:49:34 +00:00
|
|
|
processComputable(buyable as T, "style");
|
|
|
|
processComputable(buyable as T, "mark");
|
|
|
|
processComputable(buyable as T, "small");
|
2022-01-14 04:25:47 +00:00
|
|
|
|
2022-02-27 19:49:34 +00:00
|
|
|
buyable[GatherProps] = function (this: GenericBuyable) {
|
|
|
|
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
|
|
|
this;
|
2022-03-13 22:09:09 +00:00
|
|
|
return {
|
|
|
|
display,
|
|
|
|
visibility,
|
|
|
|
style: unref(style),
|
|
|
|
classes,
|
|
|
|
onClick,
|
|
|
|
canClick,
|
|
|
|
small,
|
|
|
|
mark,
|
|
|
|
id
|
|
|
|
};
|
2022-02-27 19:49:34 +00:00
|
|
|
};
|
2022-01-14 04:25:47 +00:00
|
|
|
|
2022-02-27 19:49:34 +00:00
|
|
|
return buyable as unknown as Buyable<T>;
|
2022-04-23 23:20:15 +00:00
|
|
|
});
|
2022-01-14 04:25:47 +00:00
|
|
|
}
|