Add decorators to decoratable features

This commit is contained in:
Seth Posner 2023-02-25 16:48:36 -08:00
parent 40d2bcf55d
commit 691a68ecf2
12 changed files with 213 additions and 350 deletions

View file

@ -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) {

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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>;
});
}

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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>;
});
}

View file

@ -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
/*----====----*/

View file

@ -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) {

View file

@ -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
};
};

View file

@ -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
};
};

View file

@ -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
};
};