diff --git a/CHANGELOG.md b/CHANGELOG.md index ae41644..4fa0c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Requirements can use them so repeatables and challenges can be "buy max" without any extra effort - Conversions now use formulas instead of the old scaling functions system, allowing for arbitrary functions that are much easier to follow - Modifiers have a new getFormula property +- Feature decorators, which simplify the process of adding extra values to features - Action feature, which is a clickable with a cooldown - ETA util (calculates time until a specific amount of a resource, based on its current gain rate) - createCollapsibleAchievements util diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index ccbbc37..eb7ad60 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -2,6 +2,7 @@ import { computed } from "@vue/reactivity"; import { isArray } from "@vue/shared"; import Select from "components/fields/Select.vue"; import AchievementComponent from "features/achievements/Achievement.vue"; +import { Decorator, GenericDecorator } from "features/decorators/common"; import { CoercableComponent, Component, @@ -137,9 +138,11 @@ export type GenericAchievement = Replace< * @param optionsFunc Achievement options. */ export function createAchievement( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: GenericDecorator[] ): Achievement { const earned = persistent(false, false); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const achievement = optionsFunc?.call(feature, feature) ?? @@ -148,6 +151,10 @@ export function createAchievement( achievement.type = AchievementType; achievement[Component] = AchievementComponent as GenericComponent; + for (const decorator of decorators) { + decorator.preConstruct?.(achievement); + } + achievement.earned = earned; achievement.complete = function () { earned.value = true; @@ -179,6 +186,8 @@ export function createAchievement( } }; + Object.assign(achievement, decoratedData); + processComputable(achievement as T, "visibility"); setDefault(achievement, "visibility", Visibility.Visible); const visibility = achievement.visibility as ProcessedComputable; @@ -219,6 +228,11 @@ export function createAchievement( processComputable(achievement as T, "showPopups"); setDefault(achievement, "showPopups", true); + 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, @@ -242,7 +256,8 @@ export function createAchievement( classes, mark, small, - id + id, + ...decoratedProps }; }; diff --git a/src/features/action.tsx b/src/features/action.tsx index 31dd6a1..1dc9498 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, GenericDecorator } from "./decorators/common"; /** A symbol used to identify {@link Action} features. */ export const ActionType = Symbol("Action"); @@ -102,9 +103,11 @@ export type GenericAction = Replace< * @param optionsFunc Action options. */ export function createAction( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: GenericDecorator[] ): Action { const progress = persistent(0); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const action = optionsFunc?.call(feature, feature) ?? @@ -116,8 +119,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); @@ -229,6 +237,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, @@ -252,7 +265,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 82718eb..e2be951 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, GenericDecorator } from "features/decorators/common"; import type { CoercableComponent, GenericComponent, @@ -101,14 +102,22 @@ export type GenericBar = Replace< * @param optionsFunc Bar options. */ export function createBar( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: GenericDecorator[] ): Bar { + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const bar = optionsFunc.call(feature, feature); bar.id = getUniqueID("bar-"); bar.type = BarType; bar[Component] = BarComponent as GenericComponent; + 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"); @@ -124,6 +133,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, @@ -155,7 +169,8 @@ export function createBar( baseStyle, fillStyle, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index a180139..ad6ca6a 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, GenericDecorator } from "features/decorators/common"; import type { CoercableComponent, GenericComponent, @@ -148,10 +149,12 @@ export type GenericChallenge = Replace< * @param optionsFunc Challenge options. */ export function createChallenge( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: GenericDecorator[] ): Challenge { const completions = persistent(0); const active = persistent(false, false); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const challenge = optionsFunc.call(feature, feature); @@ -159,8 +162,14 @@ export function createChallenge( challenge.type = ChallengeType; challenge[Component] = ChallengeComponent as GenericComponent; + 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) ); @@ -258,6 +267,11 @@ export function createChallenge( }); } + 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, @@ -287,7 +301,8 @@ export function createChallenge( mark, id, toggle, - requirements + requirements, + ...decoratedProps }; }; diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts index 1516965..4e3dfcf 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, GenericDecorator } from "features/decorators/common"; import type { CoercableComponent, GenericComponent, @@ -95,8 +96,10 @@ export type GenericClickable = Replace< * @param optionsFunc Clickable options. */ export function createClickable( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: GenericDecorator[] ): Clickable { + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const clickable = optionsFunc?.call(feature, feature) ?? @@ -105,6 +108,12 @@ export function createClickable( clickable.type = ClickableType; clickable[Component] = ClickableComponent as GenericComponent; + 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"); @@ -131,6 +140,11 @@ export function createClickable( }; } + 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, @@ -154,7 +168,8 @@ export function createClickable( canClick, small, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 6ea498f..c9c22c2 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -15,6 +15,7 @@ import { convertComputable, processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import type { Ref } from "vue"; import { computed, unref } from "vue"; +import { GenericDecorator } from "./decorators/common"; /** An object that configures a {@link Conversion}. */ export interface ConversionOptions { @@ -123,11 +124,16 @@ export type GenericConversion = Replace< * @see {@link createIndependentConversion}. */ export function createConversion( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: GenericDecorator[] ): Conversion { return createLazyProxy(feature => { const conversion = optionsFunc.call(feature, feature); + for (const decorator of decorators) { + decorator.preConstruct?.(conversion); + } + (conversion as GenericConversion).formula = conversion.formula( Formula.variable(conversion.baseResource) ); @@ -187,6 +193,10 @@ export function createConversion( processComputable(conversion as T, "buyMax"); setDefault(conversion, "buyMax", true); + for (const decorator of decorators) { + decorator.postConstruct?.(conversion); + } + return conversion as unknown as Conversion; }); } diff --git a/src/features/decorators/bonusDecorator.ts b/src/features/decorators/bonusDecorator.ts new file mode 100644 index 0000000..b1c38cf --- /dev/null +++ b/src/features/decorators/bonusDecorator.ts @@ -0,0 +1,97 @@ +import { Replace } from "features/feature"; +import Decimal, { DecimalSource } from "util/bignum"; +import { Computable, GetComputableType, ProcessedComputable, processComputable } from "util/computed"; +import { Ref, computed, unref } from "vue"; +import { Decorator } from "./common"; + +export interface BonusAmountFeatureOptions { + bonusAmount: Computable; + totalAmount?: Computable; +} +export interface BonusCompletionsFeatureOptions { + bonusCompletions: Computable; + totalCompletions?: Computable; +} + +export interface BaseBonusAmountFeature { + amount: Ref; + bonusAmount: ProcessedComputable; + totalAmount?: Ref; +} +export interface BaseBonusCompletionsFeature { + completions: Ref; + bonusCompletions: ProcessedComputable; + totalCompletions?: Ref; +} + +export type BonusAmountFeature = Replace< + T, { bonusAmount: GetComputableType; } +>; +export type BonusCompletionsFeature = Replace< + T, { bonusAmount: GetComputableType; } +>; + +export type GenericBonusAmountFeature = Replace< + BonusAmountFeature, + { + bonusAmount: ProcessedComputable; + totalAmount: ProcessedComputable; + } +>; +export type GenericBonusCompletionsFeature = Replace< + BonusCompletionsFeature, + { + bonusCompletions: ProcessedComputable; + totalCompletions: ProcessedComputable; + } +>; + +/** + * Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount". + * To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusAmountFeatureOptions}. + * Additionally, the base feature must have an `amount` property. + * To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusAmountFeature}. + * @example ```ts + * createRepeatable(() => ({ + * bonusAmount: noPersist(otherRepeatable.amount), + * ... + * }), bonusAmountDecorator) as GenericRepeatable & GenericBonusAmountFeature + */ +export const bonusAmountDecorator: Decorator = { + preConstruct(feature) { + if (feature.amount === undefined) { + console.error(`Decorated feature ${feature.id} does not contain the required 'amount' property"`); + } + }, + postConstruct(feature) { + processComputable(feature, "bonusAmount"); + if (feature.totalAmount === undefined) { + feature.totalAmount = computed(() => Decimal.add( + unref(feature.amount ?? 0), + unref(feature.bonusAmount as ProcessedComputable) + )); + } + } +} + +/** + * Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount". + * To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusCompletionFeatureOptions}. + * To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusCompletionFeature}. + * @example ```ts + * createChallenge(() => ({ + * bonusCompletions: noPersist(otherChallenge.completions), + * ... + * }), bonusCompletionDecorator) as GenericChallenge & GenericBonusCompletionFeature + */ +export const bonusCompletionsDecorator: Decorator = { + postConstruct(feature) { + processComputable(feature, "bonusCompletions"); + if (feature.totalCompletions === undefined) { + feature.totalCompletions = computed(() => Decimal.add( + unref(feature.completions ?? 0), + unref(feature.bonusCompletions as ProcessedComputable) + )); + } + } +} diff --git a/src/features/decorators/common.ts b/src/features/decorators/common.ts new file mode 100644 index 0000000..fb084db --- /dev/null +++ b/src/features/decorators/common.ts @@ -0,0 +1,43 @@ +import { Replace, OptionsObject } from "../feature"; +import { Computable, GetComputableType, processComputable, ProcessedComputable } from "util/computed"; +import { Persistent, State } from "game/persistence"; + +export type Decorator = { + getPersistentData?(): Record>; + preConstruct?(feature: OptionsObject): void; + postConstruct?(feature: OptionsObject): void; + getGatheredProps?(feature: OptionsObject): Partial> +} + +export type GenericDecorator = Decorator; + +export interface EffectFeatureOptions { + effect: Computable; +} + +export type EffectFeature = Replace< + T, { effect: GetComputableType; } +>; + +export type GenericEffectFeature = Replace< + EffectFeature, + { effect: ProcessedComputable; } +>; + +/** + * Allows the usage of an `effect` field in the decorated feature. + * To function properly, the `createFeature()` function must have its generic type extended by {@linkcode EffectFeatureOptions}. + * To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericEffectFeature}. + * @example ```ts + * createRepeatable(() => ({ + * effect() { return Decimal.pow(2, this.amount); }, + * ... + * }), effectDecorator) as GenericUpgrade & GenericEffectFeature; + * ``` + */ +export const effectDecorator: Decorator = { + postConstruct(feature) { + processComputable(feature, "effect"); + } +} + diff --git a/src/features/feature.ts b/src/features/feature.ts index db0af40..2bd1fad 100644 --- a/src/features/feature.ts +++ b/src/features/feature.ts @@ -42,7 +42,9 @@ export type Replace = S & Omit; * with "this" bound to what the type will eventually be processed into. * Intended for making lazily evaluated objects. */ -export type OptionsFunc = (obj: R) => T & Partial & ThisType; +export type OptionsFunc = (obj: R) => OptionsObject; + +export type OptionsObject = T & Partial & ThisType; let id = 0; /** diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 098b637..bc486e4 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -30,6 +30,7 @@ import { createLazyProxy } from "util/proxies"; import { coerceComponent, isCoercableComponent } from "util/vue"; import type { Ref } from "vue"; import { computed, unref } from "vue"; +import { Decorator, GenericDecorator } from "./decorators/common"; /** A symbol used to identify {@link Repeatable} features. */ export const RepeatableType = Symbol("Repeatable"); @@ -129,19 +130,27 @@ export type GenericRepeatable = Replace< * @param optionsFunc Repeatable options. */ export function createRepeatable( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: GenericDecorator[] ): Repeatable { const amount = persistent(0); - return createLazyProxy(feature => { + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); + return createLazyProxy, Repeatable>(feature => { const repeatable = optionsFunc.call(feature, feature); repeatable.id = getUniqueID("repeatable-"); repeatable.type = RepeatableType; repeatable[Component] = ClickableComponent as GenericComponent; + 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( @@ -223,14 +232,12 @@ export function createRepeatable( {currDisplay.showAmount === false ? null : (

- {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} + )
)} {currDisplay.effectDisplay == null ? null : ( @@ -263,6 +270,11 @@ export function createRepeatable( 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; @@ -275,7 +287,8 @@ export function createRepeatable( canClick, small, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index 67c6892..b773ff9 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -1,3 +1,4 @@ +import { Decorator, GenericDecorator } from "features/decorators/common"; import type { CoercableComponent, GenericComponent, @@ -101,8 +102,10 @@ export type GenericTreeNode = Replace< * @param optionsFunc Tree Node options. */ export function createTreeNode( - optionsFunc?: OptionsFunc + optionsFunc?: OptionsFunc, + ...decorators: GenericDecorator[] ): TreeNode { + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const treeNode = optionsFunc?.call(feature, feature) ?? @@ -111,6 +114,12 @@ export function createTreeNode( treeNode.type = TreeNodeType; treeNode[Component] = TreeNodeComponent as GenericComponent; + 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"); @@ -122,6 +131,10 @@ export function createTreeNode( 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) { @@ -139,6 +152,7 @@ export function createTreeNode( }; } + const decoratedProps = decorators.reduce((current, next) => Object.assign(current, next.getGatheredProps?.(treeNode)), {}); treeNode[GatherProps] = function (this: GenericTreeNode) { const { display, @@ -164,7 +178,8 @@ export function createTreeNode( glowColor, canClick, mark, - id + id, + ...decoratedProps }; }; diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 5eebfc6..b8aa90f 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -1,4 +1,5 @@ import { isArray } from "@vue/shared"; +import { Decorator, GenericDecorator } from "features/decorators/common"; import type { CoercableComponent, GenericComponent, @@ -14,11 +15,13 @@ import { setDefault, Visibility } from "features/feature"; +import { createResource } from "features/resources/resource"; import UpgradeComponent from "features/upgrades/Upgrade.vue"; import type { GenericLayer } from "game/layers"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; import { +createCostRequirement, createVisibilityRequirement, payRequirements, Requirements, @@ -115,16 +118,24 @@ export type GenericUpgrade = Replace< * @param optionsFunc Upgrade options. */ export function createUpgrade( - optionsFunc: OptionsFunc + optionsFunc: OptionsFunc, + ...decorators: GenericDecorator[] ): Upgrade { const bought = persistent(false, false); + const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); return createLazyProxy(feature => { const upgrade = optionsFunc.call(feature, feature); upgrade.id = getUniqueID("upgrade-"); upgrade.type = UpgradeType; upgrade[Component] = UpgradeComponent as GenericComponent; + 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; @@ -150,6 +161,11 @@ export function createUpgrade( 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, @@ -173,7 +189,8 @@ export function createUpgrade( bought, mark, id, - purchase + purchase, + ...decoratedProps }; };