chromatic-lattice/src/features/buyable.tsx

184 lines
6.3 KiB
TypeScript
Raw Normal View History

2022-01-14 04:25:47 +00:00
import ClickableComponent from "@/components/features/Clickable.vue";
import { Resource } from "@/features/resource";
import Decimal, { DecimalSource, format } from "@/util/bignum";
import {
Computable,
GetComputableType,
GetComputableTypeWithDefault,
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { isCoercableComponent } from "@/util/vue";
import { computed, Ref, unref } from "vue";
import {
CoercableComponent,
Component,
getUniqueID,
makePersistent,
Persistent,
2022-01-25 04:23:30 +00:00
PersistentState,
2022-01-14 04:25:47 +00:00
Replace,
setDefault,
StyleValue,
Visibility
} from "./feature";
export const BuyableType = Symbol("Buyable");
type BuyableDisplay =
| 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;
}
interface BaseBuyable extends Persistent<DecimalSource> {
id: string;
amount: Ref<DecimalSource>;
bought: Ref<boolean>;
canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>;
onClick: VoidFunction;
purchase: VoidFunction;
type: typeof BuyableType;
[Component]: typeof ClickableComponent;
}
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>>;
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], 1>;
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>(
options: T & ThisType<Buyable<T>>
): Buyable<T> {
if (options.canPurchase == null && (options.resource == null || options.cost == null)) {
console.warn(
"Cannot create buyable without a canPurchase property or a resource and cost property",
options
);
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
}
const buyable: T & Partial<BaseBuyable> = options;
makePersistent<DecimalSource>(buyable, 0);
buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType;
buyable[Component] = ClickableComponent;
2022-01-25 04:23:30 +00:00
buyable.amount = buyable[PersistentState];
2022-01-14 04:25:47 +00:00
buyable.bought = computed(() => Decimal.gt(proxy.amount.value, 0));
buyable.canAfford = computed(
() =>
proxy.resource != null &&
proxy.cost != null &&
Decimal.gte(unref<Resource>(proxy.resource).value, unref(proxy.cost))
);
if (buyable.canPurchase == null) {
buyable.canPurchase = computed(
() =>
proxy.purchaseLimit != null &&
proxy.canAfford &&
Decimal.lt(proxy.amount.value, unref(proxy.purchaseLimit))
);
}
processComputable(buyable as T, "canPurchase");
// TODO once processComputable typing works, this can be replaced
//buyable.canClick = buyable.canPurchase;
buyable.canClick = computed(() => unref(proxy.canPurchase));
2022-01-25 04:23:30 +00:00
buyable.onClick = buyable.purchase = function () {
2022-01-14 04:25:47 +00:00
if (!unref(proxy.canPurchase) || proxy.cost == null || proxy.resource == null) {
return;
}
const cost = unref(proxy.cost);
unref<Resource>(proxy.resource).value = Decimal.sub(
unref<Resource>(proxy.resource).value,
cost
);
proxy.amount.value = Decimal.add(proxy.amount.value, 1);
this.onPurchase?.(cost);
};
processComputable(buyable as T, "display");
const display = buyable.display;
buyable.display = computed(() => {
// TODO once processComputable types correctly, remove this "as X"
const currDisplay = unref(display) as BuyableDisplay;
if (
currDisplay != null &&
!isCoercableComponent(currDisplay) &&
proxy.cost != null &&
proxy.resource != null
) {
return (
<span>
<div v-if={currDisplay.title}>
<component v-is={currDisplay.title} />
</div>
<component v-is={currDisplay.description} />
<div>
<br />
Amount: {format(proxy.amount.value)} / {format(unref(proxy.purchaseLimit))}
</div>
<div v-if={currDisplay.effectDisplay}>
<br />
Currently: <component v-is={currDisplay.effectDisplay} />
</div>
<br />
Cost: {format(unref(proxy.cost))} {unref<Resource>(proxy.resource).displayName}
</span>
);
}
return null;
});
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", 1);
processComputable(buyable as T, "classes");
processComputable(buyable as T, "style");
processComputable(buyable as T, "mark");
processComputable(buyable as T, "small");
2022-01-25 04:23:30 +00:00
const proxy = createProxy(buyable as unknown as Buyable<T>);
2022-01-14 04:25:47 +00:00
return proxy;
}