From c39982b1bc213ccd2cc6a848102fb36d632ba2cd Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 24 Apr 2023 23:39:14 -0500 Subject: [PATCH] Attempted to remove some type casts from lazy proxies --- src/features/achievements/achievement.tsx | 350 ++++++++++++++++------ src/features/feature.ts | 4 +- src/util/computed.ts | 85 ++++-- 3 files changed, 324 insertions(+), 115 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 007dd0c..7d6f2ef 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, nextTick, 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. */ @@ -111,34 +106,191 @@ export interface BaseAchievement { } /** 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 = Replace< +// T & BaseAchievement, +// { +// visibility: GetComputableTypeWithDefault; +// display: GetComputableType; +// mark: GetComputableType; +// image: GetComputableType; +// style: GetComputableType; +// classes: GetComputableType; +// showPopups: GetComputableTypeWithDefault; +// } +// >; -/** A type that matches any valid {@link Achievement} object. */ -export type GenericAchievement = Replace< - Achievement, - { - visibility: ProcessedComputable; - showPopups: ProcessedComputable; - } ->; +// export interface Achievement extends AchievementOptions, BaseAchievement { +// visibility: GetComputableTypeWithDefault; +// display: GetComputableType; +// mark: GetComputableType; +// image: GetComputableType; +// style: GetComputableType; +// classes: GetComputableType; +// showPopups: GetComputableTypeWithDefault; +// } + +export type Achievement = BaseAchievement & + ProcessedFeature> & + Defaults< + Exclude, + { + showPopups: true; + } + >; + +// 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, +// ...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) ?? +// ({} as ReturnType>); +// achievement.id = getUniqueID("achievement-"); +// achievement.type = AchievementType; +// achievement[Component] = AchievementComponent as GenericComponent; + +// for (const decorator of decorators) { +// decorator.preConstruct?.(achievement); +// } + +// achievement.display = convertComputable(achievement.display, achievement); +// achievement.mark = convertComputable(achievement.mark, achievement); +// achievement.small = convertComputable(achievement.small, achievement); +// achievement.image = convertComputable(achievement.image, achievement); +// achievement.style = convertComputable(achievement.style, achievement); +// achievement.classes = convertComputable(achievement.classes, achievement); +// achievement.showPopups = convertComputable(achievement.showPopups, achievement) ?? true; + +// achievement.earned = earned; +// achievement.complete = function () { +// earned.value = true; +// achievement.onComplete?.(); +// if (achievement.display != null && unref(achievement.showPopups) === true) { +// const display = unref((achievement as GenericAchievement).display); +// let Display; +// if (isCoercableComponent(display)) { +// Display = coerceComponent(display); +// } else if (display.requirement != null) { +// Display = coerceComponent(display.requirement); +// } else { +// Display = displayRequirements(achievement.requirements ?? []); +// } +// toast.info( +//
+//

Achievement earned!

+//
+// {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} +// {/* @ts-ignore */} +// +//
+//
+// ); +// } +// }; + +// Object.assign(achievement, decoratedData); + +// const visibility = +// convertComputable(achievement.visibility, achievement) ?? Visibility.Visible; +// achievement.visibility = computed(() => { +// const display = unref(achievement.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; +// } +// }); + +// for (const decorator of decorators) { +// decorator.postConstruct?.(achievement); +// } + +// const decoratedProps = decorators.reduce( +// (current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), +// {} +// ); +// achievement[GatherProps] = function () { +// const { +// visibility, +// display, +// requirements, +// earned, +// image, +// style, +// classes, +// mark, +// small, +// id +// } = this; +// return { +// visibility, +// display, +// requirements, +// earned, +// image, +// style: unref(style), +// classes, +// mark, +// small, +// id, +// ...decoratedProps +// }; +// }; + +// if (achievement.requirements) { +// const requirements = [ +// createVisibilityRequirement(achievement), +// createBooleanRequirement(() => !achievement.earned.value), +// ...(isArray(achievement.requirements) +// ? achievement.requirements +// : [achievement.requirements]) +// ]; +// watchEffect(() => { +// if (settings.active !== player.id) return; +// if (requirementsMet(requirements)) { +// achievement.complete(); +// } +// }); +// } + +// return achievement; +// }); +// } + export function createAchievement( - optionsFunc?: OptionsFunc, + optionsFunc?: OptionsFunc, ...decorators: GenericDecorator[] ): Achievement { const earned = persistent(false, false); @@ -146,35 +298,25 @@ export function createAchievement( (current, next) => Object.assign(current, next.getPersistentData?.()), {} ); - return createLazyProxy(feature => { - const achievement = - optionsFunc?.call(feature, feature) ?? + return createLazyProxy(achievement => { + const options = + optionsFunc?.call(achievement, achievement) ?? ({} as ReturnType>); - achievement.id = getUniqueID("achievement-"); - achievement.type = AchievementType; - achievement[Component] = AchievementComponent as GenericComponent; - for (const decorator of decorators) { - decorator.preConstruct?.(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 as GenericAchievement).onComplete?.(); + if (achievement.display != null && unref(achievement.showPopups) === true) { + const display = unref((achievement as GenericAchievement).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 as GenericAchievement).requirements ?? [] + ); } toast.info(
@@ -187,19 +329,16 @@ 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); + const optionsVisibility = + convertComputable(options.visibility, options) ?? Visibility.Visible; + const visibility = computed(() => { + const display = unref(achievement.display); switch (settings.msDisplay) { default: case AchievementDisplay.All: - return unref(visibility); + return unref(optionsVisibility); case AchievementDisplay.Configurable: if ( unref(achievement.earned) && @@ -211,35 +350,28 @@ export function createAchievement( ) { return Visibility.None; } - return unref(visibility); + return unref(optionsVisibility); case AchievementDisplay.Incomplete: if (unref(achievement.earned)) { return Visibility.None; } - return unref(visibility); + return unref(optionsVisibility); 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); + decorator.preConstruct?.(achievement); } + Object.assign(achievement, decoratedData); const decoratedProps = decorators.reduce( (current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), {} ); - achievement[GatherProps] = function (this: GenericAchievement) { + + function gatherProps(this: Achievement) { const { visibility, display, @@ -265,29 +397,67 @@ export function createAchievement( id, ...decoratedProps }; - }; - - if (achievement.requirements) { - const genericAchievement = achievement as GenericAchievement; - const requirements = [ - createVisibilityRequirement(genericAchievement), - createBooleanRequirement(() => !genericAchievement.earned.value), - ...(isArray(achievement.requirements) - ? achievement.requirements - : [achievement.requirements]) - ]; - watchEffect(() => { - if (settings.active !== player.id) return; - if (requirementsMet(requirements)) { - genericAchievement.complete(); - } - }); } - return achievement as unknown as Achievement; + nextTick(() => { + for (const decorator of decorators) { + decorator.postConstruct?.(achievement); + } + + if (achievement.requirements) { + const requirements = [ + createVisibilityRequirement(achievement as GenericAchievement), + createBooleanRequirement(() => !achievement.earned.value), + ...(isArray(achievement.requirements) + ? achievement.requirements + : [achievement.requirements]) + ]; + watchEffect(() => { + if (settings.active !== player.id) return; + if (requirementsMet(requirements)) { + achievement.complete(); + } + }); + } + }); + + return { + id: getUniqueID("achievement-"), + visibility, + earned, + complete, + type: AchievementType, + [Component]: AchievementComponent as GenericComponent, + [GatherProps]: gatherProps, + requirements: options.requirements, + display: convertComputable(options.display, options), + mark: convertComputable(options.mark, options), + small: convertComputable(options.small, options), + image: convertComputable(options.image, options), + style: convertComputable(options.style, options), + classes: convertComputable(options.classes, options), + showPopups: convertComputable(options.showPopups, options) ?? true, + onComplete: options.onComplete + } /* as 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 + 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; }