Add decorators to decoratable features
This commit is contained in:
parent
40d2bcf55d
commit
691a68ecf2
12 changed files with 213 additions and 350 deletions
|
@ -1,4 +1,5 @@
|
|||
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||
import { Decorator } from "features/decorators";
|
||||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
|
@ -72,20 +73,28 @@ export type GenericAchievement = Replace<
|
|||
>;
|
||||
|
||||
export function createAchievement<T extends AchievementOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
|
||||
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>,
|
||||
...decorators: Decorator<T, BaseAchievement, GenericAchievement>[]
|
||||
): Achievement<T> {
|
||||
const earned = persistent<boolean>(false);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
achievement.id = getUniqueID("achievement-");
|
||||
achievement.type = AchievementType;
|
||||
achievement[Component] = AchievementComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(achievement);
|
||||
}
|
||||
|
||||
achievement.earned = earned;
|
||||
achievement.complete = function () {
|
||||
earned.value = true;
|
||||
};
|
||||
|
||||
Object.assign(achievement, decoratedData);
|
||||
|
||||
processComputable(achievement as T, "visibility");
|
||||
setDefault(achievement, "visibility", Visibility.Visible);
|
||||
processComputable(achievement as T, "display");
|
||||
|
@ -94,9 +103,14 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
processComputable(achievement as T, "style");
|
||||
processComputable(achievement as T, "classes");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(achievement);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), {});
|
||||
achievement[GatherProps] = function (this: GenericAchievement) {
|
||||
const { visibility, display, earned, image, style, classes, mark, id } = this;
|
||||
return { visibility, display, earned, image, style: unref(style), classes, mark, id };
|
||||
return { visibility, display, earned, image, style: unref(style), classes, mark, id, ...decoratedProps };
|
||||
};
|
||||
|
||||
if (achievement.shouldEarn) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import { coerceComponent, isCoercableComponent, render } from "util/vue";
|
|||
import { computed, Ref, ref, unref } from "vue";
|
||||
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
||||
import { ClickableOptions } from "./clickables/clickable";
|
||||
import { Decorator } from "./decorators";
|
||||
|
||||
export const ActionType = Symbol("Action");
|
||||
|
||||
|
@ -77,9 +78,11 @@ export type GenericAction = Replace<
|
|||
>;
|
||||
|
||||
export function createAction<T extends ActionOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>
|
||||
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>,
|
||||
...decorators: Decorator<T, BaseAction, GenericAction>[]
|
||||
): Action<T> {
|
||||
const progress = persistent<DecimalSource>(0);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const action = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
action.id = getUniqueID("action-");
|
||||
|
@ -89,8 +92,13 @@ export function createAction<T extends ActionOptions>(
|
|||
// Required because of display changing types
|
||||
const genericAction = action as unknown as GenericAction;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(action);
|
||||
}
|
||||
|
||||
action.isHolding = ref(false);
|
||||
action.progress = progress;
|
||||
Object.assign(action, decoratedData);
|
||||
|
||||
processComputable(action as T, "visibility");
|
||||
setDefault(action, "visibility", Visibility.Visible);
|
||||
|
@ -202,6 +210,11 @@ export function createAction<T extends ActionOptions>(
|
|||
}
|
||||
};
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(action);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(action)));
|
||||
action[GatherProps] = function (this: GenericAction) {
|
||||
const {
|
||||
display,
|
||||
|
@ -225,7 +238,8 @@ export function createAction<T extends ActionOptions>(
|
|||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import BarComponent from "features/bars/Bar.vue";
|
||||
import { Decorator } from "features/decorators";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
|
@ -71,14 +72,22 @@ export type GenericBar = Replace<
|
|||
>;
|
||||
|
||||
export function createBar<T extends BarOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseBar, GenericBar>
|
||||
optionsFunc: OptionsFunc<T, BaseBar, GenericBar>,
|
||||
...decorators: Decorator<T, BaseBar, GenericBar>[]
|
||||
): Bar<T> {
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const bar = optionsFunc();
|
||||
bar.id = getUniqueID("bar-");
|
||||
bar.type = BarType;
|
||||
bar[Component] = BarComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(bar);
|
||||
}
|
||||
|
||||
Object.assign(bar, decoratedData);
|
||||
|
||||
processComputable(bar as T, "visibility");
|
||||
setDefault(bar, "visibility", Visibility.Visible);
|
||||
processComputable(bar as T, "width");
|
||||
|
@ -94,6 +103,11 @@ export function createBar<T extends BarOptions>(
|
|||
processComputable(bar as T, "display");
|
||||
processComputable(bar as T, "mark");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(bar);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(bar)), {});
|
||||
bar[GatherProps] = function (this: GenericBar) {
|
||||
const {
|
||||
progress,
|
||||
|
@ -125,7 +139,8 @@ export function createBar<T extends BarOptions>(
|
|||
baseStyle,
|
||||
fillStyle,
|
||||
mark,
|
||||
id
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,269 +0,0 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
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 type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, 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";
|
||||
import { Decorator } from "./decorators";
|
||||
|
||||
export const BuyableType = Symbol("Buyable");
|
||||
|
||||
export type BuyableDisplay =
|
||||
| CoercableComponent
|
||||
| {
|
||||
title?: CoercableComponent;
|
||||
description?: CoercableComponent;
|
||||
effectDisplay?: CoercableComponent;
|
||||
showAmount?: boolean;
|
||||
};
|
||||
|
||||
export interface BuyableOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
cost?: Computable<DecimalSource>;
|
||||
resource?: Resource;
|
||||
canPurchase?: Computable<boolean>;
|
||||
purchaseLimit?: Computable<DecimalSource>;
|
||||
initialValue?: DecimalSource;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
mark?: Computable<boolean | string>;
|
||||
small?: Computable<boolean>;
|
||||
display?: Computable<BuyableDisplay>;
|
||||
onPurchase?: (cost: DecimalSource | undefined) => void;
|
||||
}
|
||||
|
||||
export interface BaseBuyable {
|
||||
id: string;
|
||||
amount: Persistent<DecimalSource>;
|
||||
maxed: Ref<boolean>;
|
||||
canAfford: Ref<boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
onClick: VoidFunction;
|
||||
purchase: VoidFunction;
|
||||
type: typeof BuyableType;
|
||||
[Component]: typeof ClickableComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
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"], Decimal>;
|
||||
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>(
|
||||
optionsFunc: OptionsFunc<T, BaseBuyable, GenericBuyable>,
|
||||
...decorators: Decorator<T, BaseBuyable, GenericBuyable>[]
|
||||
): Buyable<T> {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
|
||||
const persistents = decorators.reduce((current, next) => Object.assign(current, next.getPersistents?.()), {});
|
||||
|
||||
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;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(buyable);
|
||||
}
|
||||
|
||||
buyable.amount = amount;
|
||||
buyable.amount[DefaultValue] = buyable.initialValue ?? 0;
|
||||
|
||||
Object.assign(buyable, persistents);
|
||||
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
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 =
|
||||
buyable.onClick ??
|
||||
buyable.purchase ??
|
||||
function (this: GenericBuyable) {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
if (!unref(genericBuyable.canPurchase)) {
|
||||
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);
|
||||
};
|
||||
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 <CurrDisplay />;
|
||||
}
|
||||
if (currDisplay != null && buyable.cost != null && buyable.resource != null) {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description ?? "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? "");
|
||||
|
||||
return (
|
||||
<span>
|
||||
{currDisplay.title == null ? null : (
|
||||
<div>
|
||||
<Title />
|
||||
</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.cost != null && !genericBuyable.maxed.value ? (
|
||||
<div>
|
||||
<br />
|
||||
Cost: {format(unref(genericBuyable.cost))}{" "}
|
||||
{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");
|
||||
setDefault(buyable, "purchaseLimit", Decimal.dInf);
|
||||
processComputable(buyable as T, "style");
|
||||
processComputable(buyable as T, "mark");
|
||||
processComputable(buyable as T, "small");
|
||||
|
||||
const gatheredProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(buyable)), {});
|
||||
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,
|
||||
...gatheredProps
|
||||
};
|
||||
};
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(buyable);
|
||||
}
|
||||
|
||||
return buyable as unknown as Buyable<T>;
|
||||
});
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import Toggle from "components/fields/Toggle.vue";
|
||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||
import { Decorator } from "features/decorators";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
|
@ -98,10 +99,12 @@ export type GenericChallenge = Replace<
|
|||
>;
|
||||
|
||||
export function createChallenge<T extends ChallengeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
|
||||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>,
|
||||
...decorators: Decorator<T, BaseChallenge, GenericChallenge>[]
|
||||
): Challenge<T> {
|
||||
const completions = persistent(0);
|
||||
const active = persistent(false);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const challenge = optionsFunc();
|
||||
|
||||
|
@ -120,8 +123,14 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
challenge.type = ChallengeType;
|
||||
challenge[Component] = ChallengeComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(challenge);
|
||||
}
|
||||
|
||||
challenge.completions = completions;
|
||||
challenge.active = active;
|
||||
Object.assign(challenge, decoratedData);
|
||||
|
||||
challenge.completed = computed(() =>
|
||||
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
||||
);
|
||||
|
@ -234,6 +243,11 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
});
|
||||
}
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(challenge);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(challenge)), {});
|
||||
challenge[GatherProps] = function (this: GenericChallenge) {
|
||||
const {
|
||||
active,
|
||||
|
@ -261,7 +275,8 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
canStart,
|
||||
mark,
|
||||
id,
|
||||
toggle
|
||||
toggle,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import { Decorator } from "features/decorators";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
|
@ -67,14 +68,22 @@ export type GenericClickable = Replace<
|
|||
>;
|
||||
|
||||
export function createClickable<T extends ClickableOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>
|
||||
optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>,
|
||||
...decorators: Decorator<T, BaseClickable, GenericClickable>[]
|
||||
): Clickable<T> {
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const clickable = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
clickable.id = getUniqueID("clickable-");
|
||||
clickable.type = ClickableType;
|
||||
clickable[Component] = ClickableComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(clickable);
|
||||
}
|
||||
|
||||
Object.assign(clickable, decoratedData);
|
||||
|
||||
processComputable(clickable as T, "visibility");
|
||||
setDefault(clickable, "visibility", Visibility.Visible);
|
||||
processComputable(clickable as T, "canClick");
|
||||
|
@ -101,6 +110,11 @@ export function createClickable<T extends ClickableOptions>(
|
|||
};
|
||||
}
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(clickable);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(clickable)), {});
|
||||
clickable[GatherProps] = function (this: GenericClickable) {
|
||||
const {
|
||||
display,
|
||||
|
@ -124,7 +138,8 @@ export function createClickable<T extends ClickableOptions>(
|
|||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { convertComputable, processComputable } from "util/computed";
|
|||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { Decorator } from "./decorators";
|
||||
|
||||
/** An object that configures a {@link Conversion}. */
|
||||
export interface ConversionOptions {
|
||||
|
@ -135,11 +136,16 @@ export type GenericConversion = Replace<
|
|||
* @see {@link createIndependentConversion}.
|
||||
*/
|
||||
export function createConversion<T extends ConversionOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseConversion, GenericConversion>
|
||||
optionsFunc: OptionsFunc<T, BaseConversion, GenericConversion>,
|
||||
...decorators: Decorator<T, BaseConversion, GenericConversion>[]
|
||||
): Conversion<T> {
|
||||
return createLazyProxy(() => {
|
||||
const conversion = optionsFunc();
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(conversion);
|
||||
}
|
||||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
let gain = conversion.gainModifier
|
||||
|
@ -201,6 +207,10 @@ export function createConversion<T extends ConversionOptions>(
|
|||
processComputable(conversion as T, "roundUpCost");
|
||||
setDefault(conversion, "roundUpCost", true);
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(conversion);
|
||||
}
|
||||
|
||||
return conversion as unknown as Conversion<T>;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,49 +1,35 @@
|
|||
import { Replace, OptionsObject } from "./feature";
|
||||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import { Computable, GetComputableType, processComputable, ProcessedComputable } from "util/computed";
|
||||
import { AchievementOptions, BaseAchievement, GenericAchievement } from "./achievements/achievement";
|
||||
import { BarOptions, BaseBar, GenericBar } from "./bars/bar";
|
||||
import { BaseBuyable, BuyableOptions, GenericBuyable } from "./buyable";
|
||||
import { BaseChallenge, ChallengeOptions, GenericChallenge } from "./challenges/challenge";
|
||||
import { BaseClickable, ClickableOptions, GenericClickable } from "./clickables/clickable";
|
||||
import { BaseMilestone, GenericMilestone, MilestoneOptions } from "./milestones/milestone";
|
||||
import { BaseUpgrade, GenericUpgrade, UpgradeOptions } from "./upgrades/upgrade";
|
||||
import { Persistent, State } from "game/persistence";
|
||||
import { computed, Ref, unref } from "vue";
|
||||
|
||||
type FeatureOptions = AchievementOptions | BarOptions | BuyableOptions | ChallengeOptions | ClickableOptions | MilestoneOptions | UpgradeOptions;
|
||||
|
||||
type BaseFeature = BaseAchievement | BaseBar | BaseBuyable | BaseChallenge | BaseClickable | BaseMilestone | BaseUpgrade;
|
||||
|
||||
type GenericFeature = GenericAchievement | GenericBar | GenericBuyable | GenericChallenge | GenericClickable | GenericMilestone | GenericUpgrade;
|
||||
|
||||
/*----====----*/
|
||||
|
||||
export type Decorator<Options extends FeatureOptions, Base extends BaseFeature, Generic extends GenericFeature, S extends State = State> = {
|
||||
getPersistents?(): Record<string, Persistent<S>>;
|
||||
preConstruct?(feature: OptionsObject<Options,Base,Generic>): void;
|
||||
postConstruct?(feature: OptionsObject<Options,Base,Generic>): void;
|
||||
getGatheredProps?(feature: OptionsObject<Options,Base,Generic>): Partial<OptionsObject<Options,Base,Generic>>
|
||||
export type Decorator<FeatureOptions, BaseFeature = {}, GenericFeature = {}, S extends State = State> = {
|
||||
getPersistentData?(): Record<string, Persistent<S>>;
|
||||
preConstruct?(feature: OptionsObject<FeatureOptions,BaseFeature,GenericFeature>): void;
|
||||
postConstruct?(feature: OptionsObject<FeatureOptions,BaseFeature,GenericFeature>): void;
|
||||
getGatheredProps?(feature: OptionsObject<FeatureOptions,BaseFeature,GenericFeature>): Partial<OptionsObject<FeatureOptions,BaseFeature,GenericFeature>>
|
||||
}
|
||||
|
||||
/*----====----*/
|
||||
|
||||
// #region Effect Decorator
|
||||
export type EffectFeatureOptions = {
|
||||
export interface EffectFeatureOptions {
|
||||
effect: Computable<any>;
|
||||
}
|
||||
|
||||
export type EffectFeature<T extends EffectFeatureOptions, U extends BaseFeature> = Replace<
|
||||
T & U,
|
||||
{ effect: GetComputableType<T["effect"]>; }
|
||||
export type EffectFeature<T extends EffectFeatureOptions> = Replace<
|
||||
T, { effect: GetComputableType<T["effect"]>; }
|
||||
>;
|
||||
|
||||
export type GenericEffectFeature<T extends GenericFeature> = T & Replace<
|
||||
EffectFeature<EffectFeatureOptions, BaseFeature>,
|
||||
export type GenericEffectFeature = Replace<
|
||||
EffectFeature<EffectFeatureOptions>,
|
||||
{ effect: ProcessedComputable<any>; }
|
||||
>;
|
||||
|
||||
export const effectDecorator: Decorator<FeatureOptions & EffectFeatureOptions, BaseFeature, GenericFeature & BaseFeature> = {
|
||||
export const effectDecorator: Decorator<EffectFeatureOptions, {}, GenericEffectFeature> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "effect");
|
||||
}
|
||||
|
@ -52,31 +38,46 @@ export const effectDecorator: Decorator<FeatureOptions & EffectFeatureOptions, B
|
|||
|
||||
/*----====----*/
|
||||
|
||||
// #region Bonus Amount Decorator
|
||||
export interface BonusFeatureOptions {
|
||||
// #region Bonus Amount/Completions Decorator
|
||||
export interface BonusAmountFeatureOptions {
|
||||
bonusAmount: Computable<DecimalSource>;
|
||||
}
|
||||
|
||||
export type BaseBonusFeature = BaseFeature & {
|
||||
totalAmount: Ref<DecimalSource>;
|
||||
export interface BonusCompletionsFeatureOptions {
|
||||
bonusCompletions: Computable<DecimalSource>;
|
||||
}
|
||||
|
||||
export type BonusAmountFeature<T extends BonusFeatureOptions, U extends BaseBonusFeature> = Replace<
|
||||
T & U,
|
||||
{
|
||||
bonusAmount: GetComputableType<T["bonusAmount"]>;
|
||||
}
|
||||
export interface BaseBonusAmountFeature {
|
||||
amount: Ref<DecimalSource>;
|
||||
totalAmount: Ref<DecimalSource>;
|
||||
}
|
||||
export interface BaseBonusCompletionsFeature {
|
||||
completions: Ref<DecimalSource>;
|
||||
totalCompletions: Ref<DecimalSource>;
|
||||
}
|
||||
|
||||
export type BonusAmountFeature<T extends BonusAmountFeatureOptions> = Replace<
|
||||
T, { bonusAmount: GetComputableType<T["bonusAmount"]>; }
|
||||
>;
|
||||
export type BonusCompletionsFeature<T extends BonusCompletionsFeatureOptions> = Replace<
|
||||
T, { bonusAmount: GetComputableType<T["bonusCompletions"]>; }
|
||||
>;
|
||||
|
||||
export type GenericBonusFeature<T extends GenericFeature> = Replace<
|
||||
T & BonusAmountFeature<BonusFeatureOptions, BaseBonusFeature>,
|
||||
export type GenericBonusAmountFeature = Replace<
|
||||
BonusAmountFeature<BonusAmountFeatureOptions>,
|
||||
{
|
||||
bonusAmount: ProcessedComputable<DecimalSource>;
|
||||
totalAmount: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
export type GenericBonusCompletionsFeature = Replace<
|
||||
BonusCompletionsFeature<BonusCompletionsFeatureOptions>,
|
||||
{
|
||||
bonusCompletions: ProcessedComputable<DecimalSource>;
|
||||
totalCompletions: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
export const bonusAmountDecorator: Decorator<FeatureOptions & BonusFeatureOptions, BaseBonusFeature & {amount: ProcessedComputable<DecimalSource>}, GenericFeature & BaseBonusFeature & {amount: ProcessedComputable<DecimalSource>}> = {
|
||||
export const bonusAmountDecorator: Decorator<BonusAmountFeatureOptions, BaseBonusAmountFeature, GenericBonusAmountFeature> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "bonusAmount");
|
||||
if (feature.totalAmount === undefined) {
|
||||
|
@ -87,30 +88,17 @@ export const bonusAmountDecorator: Decorator<FeatureOptions & BonusFeatureOption
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bonusCompletionsDecorator: Decorator<FeatureOptions & BonusFeatureOptions, BaseBonusFeature & {completions: ProcessedComputable<DecimalSource>}, GenericFeature & BaseBonusFeature & {completions: ProcessedComputable<DecimalSource>}> = {
|
||||
export const bonusCompletionsDecorator: Decorator<BonusAmountFeatureOptions, BaseBonusCompletionsFeature, GenericBonusCompletionsFeature> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "bonusAmount");
|
||||
if (feature.totalAmount === undefined) {
|
||||
feature.totalAmount = computed(() => Decimal.add(
|
||||
if (feature.totalCompletions === undefined) {
|
||||
feature.totalCompletions = computed(() => Decimal.add(
|
||||
unref(feature.completions ?? 0),
|
||||
unref(feature.bonusAmount as ProcessedComputable<DecimalSource>)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bonusEarnedDecorator: Decorator<FeatureOptions & BonusFeatureOptions, BaseBonusFeature & {earned: ProcessedComputable<boolean>}, GenericFeature & BaseBonusFeature & {earned: ProcessedComputable<boolean>}> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "bonusAmount");
|
||||
if (feature.totalAmount === undefined) {
|
||||
feature.totalAmount = computed(() => unref(feature.earned ?? false)
|
||||
? Decimal.add(unref(feature.bonusAmount as ProcessedComputable<DecimalSource>), 1)
|
||||
: unref(feature.bonusAmount as ProcessedComputable<DecimalSource>)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
/*----====----*/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Select from "components/fields/Select.vue";
|
||||
import { Decorator } from "features/decorators";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
|
@ -92,16 +93,23 @@ export type GenericMilestone = Replace<
|
|||
>;
|
||||
|
||||
export function createMilestone<T extends MilestoneOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseMilestone, GenericMilestone>
|
||||
optionsFunc?: OptionsFunc<T, BaseMilestone, GenericMilestone>,
|
||||
...decorators: Decorator<T, BaseMilestone, GenericMilestone>[]
|
||||
): Milestone<T> {
|
||||
const earned = persistent<boolean>(false);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
milestone.id = getUniqueID("milestone-");
|
||||
milestone.type = MilestoneType;
|
||||
milestone[Component] = MilestoneComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(milestone);
|
||||
}
|
||||
|
||||
milestone.earned = earned;
|
||||
Object.assign(milestone, decoratedData);
|
||||
milestone.complete = function () {
|
||||
const genericMilestone = milestone as GenericMilestone;
|
||||
earned.value = true;
|
||||
|
@ -160,9 +168,14 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
processComputable(milestone as T, "display");
|
||||
processComputable(milestone as T, "showPopups");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(milestone);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next?.getGatheredProps?.(milestone)), {});
|
||||
milestone[GatherProps] = function (this: GenericMilestone) {
|
||||
const { visibility, display, style, classes, earned, id } = this;
|
||||
return { visibility, display, style: unref(style), classes, earned, id };
|
||||
return { visibility, display, style: unref(style), classes, earned, id, ...decoratedProps };
|
||||
};
|
||||
|
||||
if (milestone.shouldEarn) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { createLazyProxy } from "util/proxies";
|
|||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { Decorator, GenericBonusAmountFeature } from "./decorators";
|
||||
|
||||
/** A symbol used to identify {@link Repeatable} features. */
|
||||
export const RepeatableType = Symbol("Repeatable");
|
||||
|
@ -118,9 +119,11 @@ export type GenericRepeatable = Replace<
|
|||
* @param optionsFunc Repeatable options.
|
||||
*/
|
||||
export function createRepeatable<T extends RepeatableOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>
|
||||
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>,
|
||||
...decorators: Decorator<T, BaseRepeatable, GenericRepeatable>[]
|
||||
): Repeatable<T> {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const repeatable = optionsFunc();
|
||||
|
||||
|
@ -128,9 +131,15 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
repeatable.type = RepeatableType;
|
||||
repeatable[Component] = ClickableComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(repeatable);
|
||||
}
|
||||
|
||||
repeatable.amount = amount;
|
||||
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
||||
|
||||
Object.assign(repeatable, decoratedData);
|
||||
|
||||
const limitRequirement = {
|
||||
requirementMet: computed(() =>
|
||||
Decimal.sub(
|
||||
|
@ -212,14 +221,17 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
{currDisplay.showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{unref(genericRepeatable.limit) === Decimal.dInf ? (
|
||||
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
||||
) : (
|
||||
<>
|
||||
Amount: {formatWhole(genericRepeatable.amount.value)} /{" "}
|
||||
{formatWhole(unref(genericRepeatable.limit))}
|
||||
</>
|
||||
)}
|
||||
joinJSX(
|
||||
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>,
|
||||
{unref(genericRepeatable.limit) !== Decimal.dInf ? (
|
||||
<> / {formatWhole(unref(genericRepeatable.limit))}</>
|
||||
) : undefined},
|
||||
{(genericRepeatable as GenericRepeatable & GenericBonusAmountFeature).bonusAmount == null ? null : (
|
||||
Decimal.gt(unref((genericRepeatable as GenericRepeatable & GenericBonusAmountFeature).bonusAmount), 0) ? (
|
||||
<> + {formatWhole(unref((genericRepeatable as GenericRepeatable & GenericBonusAmountFeature).bonusAmount))}</>
|
||||
) : undefined)
|
||||
}
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.effectDisplay == null ? null : (
|
||||
|
@ -254,6 +266,11 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
processComputable(repeatable as T, "small");
|
||||
processComputable(repeatable as T, "maximize");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(repeatable);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(repeatable)), {});
|
||||
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
||||
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
||||
this;
|
||||
|
@ -266,7 +283,8 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Decorator } from "features/decorators";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import type { Link } from "features/links/links";
|
||||
|
@ -66,14 +67,22 @@ export type GenericTreeNode = Replace<
|
|||
>;
|
||||
|
||||
export function createTreeNode<T extends TreeNodeOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>
|
||||
optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>,
|
||||
...decorators: Decorator<T, BaseTreeNode, GenericTreeNode>[]
|
||||
): TreeNode<T> {
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const treeNode = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
treeNode.id = getUniqueID("treeNode-");
|
||||
treeNode.type = TreeNodeType;
|
||||
treeNode[Component] = TreeNodeComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(treeNode);
|
||||
}
|
||||
|
||||
Object.assign(decoratedData);
|
||||
|
||||
processComputable(treeNode as T, "visibility");
|
||||
setDefault(treeNode, "visibility", Visibility.Visible);
|
||||
processComputable(treeNode as T, "canClick");
|
||||
|
@ -85,6 +94,10 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
|||
processComputable(treeNode as T, "style");
|
||||
processComputable(treeNode as T, "mark");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(treeNode);
|
||||
}
|
||||
|
||||
if (treeNode.onClick) {
|
||||
const onClick = treeNode.onClick.bind(treeNode);
|
||||
treeNode.onClick = function (e) {
|
||||
|
@ -102,6 +115,7 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
|||
};
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(treeNode)), {});
|
||||
treeNode[GatherProps] = function (this: GenericTreeNode) {
|
||||
const {
|
||||
display,
|
||||
|
@ -127,7 +141,8 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
|||
glowColor,
|
||||
canClick,
|
||||
mark,
|
||||
id
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import { Decorator } from "features/decorators";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
|
@ -85,16 +86,24 @@ export type GenericUpgrade = Replace<
|
|||
>;
|
||||
|
||||
export function createUpgrade<T extends UpgradeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>
|
||||
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>,
|
||||
...decorators: Decorator<T, BaseUpgrade, GenericUpgrade>[]
|
||||
): Upgrade<T> {
|
||||
const bought = persistent<boolean>(false);
|
||||
const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {});
|
||||
return createLazyProxy(() => {
|
||||
const upgrade = optionsFunc();
|
||||
upgrade.id = getUniqueID("upgrade-");
|
||||
upgrade.type = UpgradeType;
|
||||
upgrade[Component] = UpgradeComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(upgrade);
|
||||
}
|
||||
|
||||
upgrade.bought = bought;
|
||||
Object.assign(upgrade, decoratedData);
|
||||
|
||||
upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
|
||||
upgrade.purchase = function () {
|
||||
const genericUpgrade = upgrade as GenericUpgrade;
|
||||
|
@ -120,6 +129,11 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
processComputable(upgrade as T, "display");
|
||||
processComputable(upgrade as T, "mark");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(upgrade);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(upgrade)), {});
|
||||
upgrade[GatherProps] = function (this: GenericUpgrade) {
|
||||
const {
|
||||
display,
|
||||
|
@ -143,7 +157,8 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
bought,
|
||||
mark,
|
||||
id,
|
||||
purchase
|
||||
purchase,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue