diff --git a/src/data/common.tsx b/src/data/common.tsx index 2430c07..8cc7853 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -437,7 +437,7 @@ export function estimateTime( const currTarget = unref(processedTarget); if (Decimal.gte(resource.value, currTarget)) { return "Now"; - } else if (Decimal.lt(currRate, 0)) { + } else if (Decimal.lte(currRate, 0)) { return "Never"; } return formatTime(Decimal.sub(currTarget, resource.value).div(currRate)); diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 007dd0c..7a38331 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -9,7 +9,6 @@ import { GatherProps, GenericComponent, OptionsFunc, - Replace, StyleValue, Visibility, getUniqueID, @@ -30,16 +29,10 @@ import { } from "game/requirements"; import settings, { registerSettingField } from "game/settings"; import { camelToTitle } from "util/common"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { processComputable } from "util/computed"; +import { Computable, Defaults, ProcessedFeature, convertComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { coerceComponent, isCoercableComponent } from "util/vue"; -import { unref, watchEffect } from "vue"; +import { ComputedRef, unref, watchEffect } from "vue"; import { useToast } from "vue-toastification"; const toast = useToast(); @@ -98,6 +91,8 @@ export interface AchievementOptions { export interface BaseAchievement { /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ id: string; + /** Whether this achievement should be visible. */ + visibility: ComputedRef; /** Whether or not this achievement has been earned. */ earned: Persistent; /** A function to complete this achievement. */ @@ -110,35 +105,20 @@ export interface BaseAchievement { [GatherProps]: () => Record; } -/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */ -export type Achievement = Replace< - T & BaseAchievement, - { - visibility: GetComputableTypeWithDefault; - display: GetComputableType; - mark: GetComputableType; - image: GetComputableType; - style: GetComputableType; - classes: GetComputableType; - showPopups: GetComputableTypeWithDefault; - } ->; +export type Achievement = BaseAchievement & + ProcessedFeature> & + Defaults< + Exclude, + { + showPopups: true; + } + >; -/** A type that matches any valid {@link Achievement} object. */ -export type GenericAchievement = Replace< - Achievement, - { - visibility: ProcessedComputable; - showPopups: ProcessedComputable; - } ->; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GenericAchievement = Achievement; -/** - * Lazily creates an achievement with the given options. - * @param optionsFunc Achievement options. - */ export function createAchievement( - optionsFunc?: OptionsFunc, + optionsFunc?: OptionsFunc, ...decorators: GenericDecorator[] ): Achievement { const earned = persistent(false, false); @@ -147,34 +127,81 @@ export function createAchievement( {} ); return createLazyProxy(feature => { - const achievement = + const { visibility, display, mark, small, image, style, classes, showPopups, ...options } = optionsFunc?.call(feature, feature) ?? ({} as ReturnType>); - achievement.id = getUniqueID("achievement-"); - achievement.type = AchievementType; - achievement[Component] = AchievementComponent as GenericComponent; + + const optionsVisibility = convertComputable(visibility, feature) ?? Visibility.Visible; + const processedVisibility = computed(() => { + const display = unref(achievement.display); + switch (settings.msDisplay) { + default: + case AchievementDisplay.All: + return unref(optionsVisibility); + case AchievementDisplay.Configurable: + if ( + unref(achievement.earned) && + !( + display != null && + typeof display == "object" && + "optionsDisplay" in (display as Record) + ) + ) { + return Visibility.None; + } + return unref(optionsVisibility); + case AchievementDisplay.Incomplete: + if (unref(achievement.earned)) { + return Visibility.None; + } + return unref(optionsVisibility); + case AchievementDisplay.None: + return Visibility.None; + } + }); + + const achievement = { + id: getUniqueID("achievement-"), + visibility: processedVisibility, + earned, + complete, + type: AchievementType, + [Component]: AchievementComponent as GenericComponent, + [GatherProps]: gatherProps, + display: convertComputable(display, feature), + mark: convertComputable(mark, feature), + small: convertComputable(small, feature), + image: convertComputable(image, feature), + style: convertComputable(style, feature), + classes: convertComputable(classes, feature), + showPopups: convertComputable(showPopups, feature) ?? true, + ...options + } satisfies Partial>; for (const decorator of decorators) { decorator.preConstruct?.(achievement); } + Object.assign(achievement, decoratedData); + for (const decorator of decorators) { + decorator.postConstruct?.(achievement); + } + const decoratedProps = decorators.reduce( + (current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), + {} + ); - achievement.earned = earned; - achievement.complete = function () { + function complete() { earned.value = true; - const genericAchievement = achievement as GenericAchievement; - genericAchievement.onComplete?.(); - if ( - genericAchievement.display != null && - unref(genericAchievement.showPopups) === true - ) { - const display = unref(genericAchievement.display); + achievement.onComplete?.(); + if (achievement.display != null && unref(achievement.showPopups) === true) { + const display = unref(achievement.display); let Display; if (isCoercableComponent(display)) { Display = coerceComponent(display); } else if (display.requirement != null) { Display = coerceComponent(display.requirement); } else { - Display = displayRequirements(genericAchievement.requirements ?? []); + Display = displayRequirements(achievement.requirements ?? []); } toast.info(
@@ -187,59 +214,9 @@ export function createAchievement(
); } - }; - - Object.assign(achievement, decoratedData); - - processComputable(achievement as T, "visibility"); - setDefault(achievement, "visibility", Visibility.Visible); - const visibility = achievement.visibility as ProcessedComputable; - achievement.visibility = computed(() => { - const display = unref((achievement as GenericAchievement).display); - switch (settings.msDisplay) { - default: - case AchievementDisplay.All: - return unref(visibility); - case AchievementDisplay.Configurable: - if ( - unref(achievement.earned) && - !( - display != null && - typeof display == "object" && - "optionsDisplay" in (display as Record) - ) - ) { - return Visibility.None; - } - return unref(visibility); - case AchievementDisplay.Incomplete: - if (unref(achievement.earned)) { - return Visibility.None; - } - return unref(visibility); - case AchievementDisplay.None: - return Visibility.None; - } - }); - - processComputable(achievement as T, "display"); - processComputable(achievement as T, "mark"); - processComputable(achievement as T, "small"); - processComputable(achievement as T, "image"); - processComputable(achievement as T, "style"); - processComputable(achievement as T, "classes"); - 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) { + function gatherProps(this: GenericAchievement): Record { const { visibility, display, @@ -265,13 +242,12 @@ export function createAchievement( id, ...decoratedProps }; - }; + } - if (achievement.requirements) { - const genericAchievement = achievement as GenericAchievement; + if (achievement.requirements != null) { const requirements = [ - createVisibilityRequirement(genericAchievement), - createBooleanRequirement(() => !genericAchievement.earned.value), + createVisibilityRequirement(achievement), + createBooleanRequirement(() => !earned.value), ...(isArray(achievement.requirements) ? achievement.requirements : [achievement.requirements]) @@ -279,15 +255,35 @@ export function createAchievement( watchEffect(() => { if (settings.active !== player.id) return; if (requirementsMet(requirements)) { - genericAchievement.complete(); + achievement.complete(); } }); } - return achievement as unknown as Achievement; + return achievement; }); } +const ach = createAchievement(ach => ({ + image: "", + showPopups: computed(() => false), + small: () => true, + foo: "bar", + bar: () => "foo" +})); +ach; +ach.image; // string +ach.showPopups; // ComputedRef +ach.small; // ComputedRef +ach.foo; // "bar" +ach.bar; // () => "foo" +ach.mark; // TS should yell about this not existing (or at least mark it undefined) +ach.visibility; // ComputedRef + +const badAch = createAchievement(() => ({ + requirements: "foo" +})); + declare module "game/settings" { interface Settings { msDisplay: AchievementDisplay; diff --git a/src/features/feature.ts b/src/features/feature.ts index 429629a..dc7b6bc 100644 --- a/src/features/feature.ts +++ b/src/features/feature.ts @@ -42,9 +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) => OptionsObject; +export type OptionsFunc = (this: R, obj: R) => OptionsObject; -export type OptionsObject = T & Partial & ThisType; +export type OptionsObject = Exclude & Partial; let id = 0; /** diff --git a/src/util/computed.ts b/src/util/computed.ts index 8a9ad62..eeaa95e 100644 --- a/src/util/computed.ts +++ b/src/util/computed.ts @@ -1,6 +1,6 @@ import type { JSXFunction } from "features/feature"; import { isFunction } from "util/common"; -import type { Ref } from "vue"; +import type { ComputedRef, Ref } from "vue"; import { computed } from "vue"; export const DoNotCache = Symbol("DoNotCache"); @@ -26,32 +26,71 @@ export type ComputableKeysOf = Pick< }[keyof T] >; +export type ProcessedFeature = { + [K in keyof S]: K extends keyof T + ? T[K] extends Computable + ? S[K] extends () => infer Q + ? ComputedRef + : S[K] + : S[K] + : S[K]; +}; + +export type Defaults = { + [K in keyof S]: K extends keyof T ? (T[K] extends undefined ? S[K] : T[K]) : S[K]; +}; + // TODO fix the typing of this function, such that casting isn't necessary and can be used to // detect if a createX function is validly written -export function processComputable>( - obj: T, - key: S -): asserts obj is T & { [K in S]: ProcessedComputable> } { - const computable = obj[key]; - if ( - isFunction(computable) && - computable.length === 0 && - !(computable as unknown as JSXFunction)[DoNotCache] - ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - obj[key] = computed(computable.bind(obj)); - } else if (isFunction(computable)) { - obj[key] = computable.bind(obj) as unknown as T[S]; - (obj[key] as unknown as JSXFunction)[DoNotCache] = true; - } +// export function processComputable< +// T extends object, +// S extends keyof { +// [K in keyof T]: T[K] extends Computable ? K : never; +// }, +// R = T[S] +// >( +// obj: T, +// key: S +// ): asserts obj is { +// [K in keyof T]: K extends keyof S ? S[K] extends ProcessedComputable> : T[K]; +// } { +// const computable = obj[key]; +// if ( +// isFunction(computable) && +// computable.length === 0 && +// !(computable as unknown as JSXFunction)[DoNotCache] +// ) { +// // eslint-disable-next-line @typescript-eslint/ban-ts-comment +// // @ts-ignore +// obj[key] = computed(computable.bind(obj)); +// } else if (isFunction(computable)) { +// obj[key] = computable.bind(obj) as unknown as T[S]; +// (obj[key] as unknown as JSXFunction)[DoNotCache] = true; +// } +// } + +function isJSXFunction(value: unknown): value is JSXFunction { + return typeof value === "function" && DoNotCache in value && value[DoNotCache] === true; } -export function convertComputable(obj: Computable): ProcessedComputable { - if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore +export function convertComputable( + obj: Computable>, + thisArg?: object +): typeof obj extends JSXFunction ? typeof obj : ProcessedComputable; +export function convertComputable(obj: undefined, thisArg?: object): undefined; +export function convertComputable( + obj: Computable | undefined, + thisArg?: object +): (typeof obj extends JSXFunction ? typeof obj : ProcessedComputable) | undefined; +export function convertComputable( + obj: Computable | undefined, + thisArg?: object +) /*: (typeof obj extends JSXFunction ? typeof obj : ProcessedComputable) | undefined*/ { + if (isFunction(obj) && !isJSXFunction(obj)) { + if (thisArg != null) { + obj = obj.bind(thisArg); + } obj = computed(obj); } - return obj as ProcessedComputable; + return obj; }