From 691a68ecf2d44bb771fa3bcb968f68785100ee82 Mon Sep 17 00:00:00 2001 From: Seth Posner Date: Sat, 25 Feb 2023 16:48:36 -0800 Subject: [PATCH] Add decorators to decoratable features --- src/features/achievements/achievement.tsx | 18 +- src/features/action.tsx | 18 +- src/features/bars/bar.ts | 19 +- src/features/buyable.tsx | 269 ---------------------- src/features/challenges/challenge.tsx | 19 +- src/features/clickables/clickable.ts | 19 +- src/features/conversion.ts | 12 +- src/features/decorators.ts | 96 ++++---- src/features/milestones/milestone.tsx | 17 +- src/features/repeatable.tsx | 38 ++- src/features/trees/tree.ts | 19 +- src/features/upgrades/upgrade.ts | 19 +- 12 files changed, 213 insertions(+), 350 deletions(-) delete mode 100644 src/features/buyable.tsx diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 500e93f..c28003c 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -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( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: Decorator[] ): Achievement { const earned = persistent(false); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(() => { const achievement = optionsFunc?.() ?? ({} as ReturnType>); 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( 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) { diff --git a/src/features/action.tsx b/src/features/action.tsx index 5d7e555..ed098bb 100644 --- a/src/features/action.tsx +++ b/src/features/action.tsx @@ -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( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: Decorator[] ): Action { const progress = persistent(0); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(() => { const action = optionsFunc?.() ?? ({} as ReturnType>); action.id = getUniqueID("action-"); @@ -89,8 +92,13 @@ export function createAction( // 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( } }; + 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( canClick, small, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts index f0436f4..107fe1f 100644 --- a/src/features/bars/bar.ts +++ b/src/features/bars/bar.ts @@ -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( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: Decorator[] ): Bar { + 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( 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( baseStyle, fillStyle, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx deleted file mode 100644 index 4457993..0000000 --- a/src/features/buyable.tsx +++ /dev/null @@ -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; - cost?: Computable; - resource?: Resource; - canPurchase?: Computable; - purchaseLimit?: Computable; - initialValue?: DecimalSource; - classes?: Computable>; - style?: Computable; - mark?: Computable; - small?: Computable; - display?: Computable; - onPurchase?: (cost: DecimalSource | undefined) => void; -} - -export interface BaseBuyable { - id: string; - amount: Persistent; - maxed: Ref; - canAfford: Ref; - canClick: ProcessedComputable; - onClick: VoidFunction; - purchase: VoidFunction; - type: typeof BuyableType; - [Component]: typeof ClickableComponent; - [GatherProps]: () => Record; -} - -export type Buyable = Replace< - T & BaseBuyable, - { - visibility: GetComputableTypeWithDefault; - cost: GetComputableType; - resource: GetComputableType; - canPurchase: GetComputableTypeWithDefault>; - purchaseLimit: GetComputableTypeWithDefault; - classes: GetComputableType; - style: GetComputableType; - mark: GetComputableType; - small: GetComputableType; - display: Ref; - } ->; - -export type GenericBuyable = Replace< - Buyable, - { - visibility: ProcessedComputable; - canPurchase: ProcessedComputable; - purchaseLimit: ProcessedComputable; - } ->; - -export function createBuyable( - optionsFunc: OptionsFunc, - ...decorators: Decorator[] -): Buyable { - const amount = persistent(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> | 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; - 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 ; - } - 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 ( - - {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.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>; - }); -} diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index ef4c77d..d703014 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -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 }; }; diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts index c7c83e3..8df5285 100644 --- a/src/features/clickables/clickable.ts +++ b/src/features/clickables/clickable.ts @@ -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 }; }; diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 97637fa..baa1ce2 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -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>; }); } diff --git a/src/features/decorators.ts b/src/features/decorators.ts index 774312c..4eeadb4 100644 --- a/src/features/decorators.ts +++ b/src/features/decorators.ts @@ -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 /*----====----*/ diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index 6ea736b..d76925e 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -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) { diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 96bea11..91e1052 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -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 }; }; diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index bcbf333..caaeee3 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -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 }; }; diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 5eda0e0..14ea1ec 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -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 }; };