From c39982b1bc213ccd2cc6a848102fb36d632ba2cd Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 24 Apr 2023 23:39:14 -0500 Subject: [PATCH 1/4] 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; } -- 2.45.2 From cf069cee6117423c550207889cba7cc85b2f681a Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 26 Apr 2023 20:19:14 -0500 Subject: [PATCH 2/4] Update PoC --- src/features/achievements/achievement.tsx | 320 +++++----------------- 1 file changed, 74 insertions(+), 246 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 7d6f2ef..2d00c49 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -32,7 +32,7 @@ import { camelToTitle } from "util/common"; import { Computable, Defaults, ProcessedFeature, convertComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { coerceComponent, isCoercableComponent } from "util/vue"; -import { ComputedRef, nextTick, unref, watchEffect } from "vue"; +import { ComputedRef, unref, watchEffect } from "vue"; import { useToast } from "vue-toastification"; const toast = useToast(); @@ -105,30 +105,6 @@ 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 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< @@ -141,154 +117,6 @@ export type Achievement = BaseAchievement & // 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, ...decorators: GenericDecorator[] @@ -298,42 +126,13 @@ export function createAchievement( (current, next) => Object.assign(current, next.getPersistentData?.()), {} ); - return createLazyProxy(achievement => { - const options = - optionsFunc?.call(achievement, achievement) ?? + return createLazyProxy(feature => { + const { visibility, display, mark, small, image, style, classes, showPopups, ...options } = + optionsFunc?.call(feature, feature) ?? ({} as ReturnType>); - function complete() { - earned.value = true; - (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( - (achievement as GenericAchievement).requirements ?? [] - ); - } - toast.info( -
-

Achievement earned!

-
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - -
-
- ); - } - } - - const optionsVisibility = - convertComputable(options.visibility, options) ?? Visibility.Visible; - const visibility = computed(() => { + const optionsVisibility = convertComputable(visibility, feature) ?? Visibility.Visible; + const processedVisibility = computed(() => { const display = unref(achievement.display); switch (settings.msDisplay) { default: @@ -361,17 +160,61 @@ export function createAchievement( } }); + 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); + function complete() { + earned.value = true; + 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(achievement.requirements ?? []); + } + toast.info( +
+

Achievement earned!

+
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + +
+
+ ); + } + } + const decoratedProps = decorators.reduce( (current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), {} ); - function gatherProps(this: Achievement) { + function gatherProps(this: Achievement): Record { const { visibility, display, @@ -399,46 +242,27 @@ export function createAchievement( }; } - nextTick(() => { - for (const decorator of decorators) { - decorator.postConstruct?.(achievement); - } + 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(); - } - }); - } - }); + if (achievement.requirements != null) { + const requirements = [ + createVisibilityRequirement(achievement), + createBooleanRequirement(() => !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*/; + return achievement; }); } @@ -458,6 +282,10 @@ 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; -- 2.45.2 From a8c550c5512cf1791002638e43d1220c9e7cd279 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 26 Apr 2023 20:28:01 -0500 Subject: [PATCH 3/4] Re-order some things --- src/features/achievements/achievement.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 2d00c49..7a38331 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -182,6 +182,13 @@ export function createAchievement( 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)), + {} + ); function complete() { earned.value = true; @@ -209,12 +216,7 @@ export function createAchievement( } } - const decoratedProps = decorators.reduce( - (current, next) => Object.assign(current, next.getGatheredProps?.(achievement)), - {} - ); - - function gatherProps(this: Achievement): Record { + function gatherProps(this: GenericAchievement): Record { const { visibility, display, @@ -242,10 +244,6 @@ export function createAchievement( }; } - for (const decorator of decorators) { - decorator.postConstruct?.(achievement); - } - if (achievement.requirements != null) { const requirements = [ createVisibilityRequirement(achievement), -- 2.45.2 From 2f8c37d73014a451e8f4f3c1888696d5d4d3ce24 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 27 Apr 2023 22:48:40 -0500 Subject: [PATCH 4/4] Fix bug with estimateTime not showing "Never" when it should --- src/data/common.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)); -- 2.45.2