From c39982b1bc213ccd2cc6a848102fb36d632ba2cd Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 24 Apr 2023 23:39:14 -0500 Subject: [PATCH 01/83] 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; } From 36fa4ece65e3698e4e89f05922c6e5c87c60c5fc Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Tue, 25 Apr 2023 23:51:18 -0500 Subject: [PATCH 02/83] Fix mouse leave deselecting nodes --- src/features/boards/Board.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/boards/Board.vue b/src/features/boards/Board.vue index 00799fe..50892b4 100644 --- a/src/features/boards/Board.vue +++ b/src/features/boards/Board.vue @@ -19,7 +19,7 @@ @touchstart="(e: TouchEvent) => mouseDown(e)" @mouseup="() => endDragging(unref(draggingNode))" @touchend.passive="() => endDragging(unref(draggingNode))" - @mouseleave="() => endDragging(unref(draggingNode))" + @mouseleave="() => endDragging(unref(draggingNode), true)" > @@ -223,7 +223,7 @@ function drag(e: MouseEvent | TouchEvent) { } } -function endDragging(node: BoardNode | null) { +function endDragging(node: BoardNode | null, mouseLeave = false) { if (props.draggingNode.value != null && props.draggingNode.value === node) { if (props.receivingNode.value == null) { props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25; @@ -241,7 +241,7 @@ function endDragging(node: BoardNode | null) { } props.setDraggingNode.value(null); - } else if (!hasDragged.value) { + } else if (!hasDragged.value && !mouseLeave) { props.state.value.selectedNode = null; props.state.value.selectedAction = null; } From cf069cee6117423c550207889cba7cc85b2f681a Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 26 Apr 2023 20:19:14 -0500 Subject: [PATCH 03/83] 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; From a8c550c5512cf1791002638e43d1220c9e7cd279 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 26 Apr 2023 20:28:01 -0500 Subject: [PATCH 04/83] 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), From 81058e10b49a9664df16471d140aa4d17fe24dff Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 27 Apr 2023 20:41:20 -0500 Subject: [PATCH 05/83] Throw error if lazy proxies get cyclical --- src/util/proxies.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/util/proxies.ts b/src/util/proxies.ts index 6e7a6f1..f712470 100644 --- a/src/util/proxies.ts +++ b/src/util/proxies.ts @@ -36,8 +36,13 @@ export function createLazyProxy( ): T { const obj: S & Partial = baseObject; let calculated = false; + let calculating = false; function calculateObj(): T { if (!calculated) { + if (calculating) { + throw new Error("Cyclical dependency detected. Cannot evaluate lazy proxy."); + } + calculating = true; Object.assign(obj, objectFunc.call(obj, obj)); calculated = true; } From 2f8c37d73014a451e8f4f3c1888696d5d4d3ce24 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 27 Apr 2023 22:48:40 -0500 Subject: [PATCH 06/83] 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)); From ff16397cc7542ccae39477b9559b54c9f606cfea Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 27 Apr 2023 22:48:40 -0500 Subject: [PATCH 07/83] 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)); From bffc27344a435818c6ab935a3304984bf8f2b7e1 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 30 Apr 2023 11:08:43 -0500 Subject: [PATCH 08/83] Fixed isInvertible and isIntegrable not working nested correctly --- src/game/formulas/formulas.ts | 7 +++++-- tests/game/formulas.test.ts | 28 ++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index bf989b1..79adfce 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -124,11 +124,14 @@ export abstract class InternalFormula { expect(formula.invert(100)).compare_tolerance(0); }); - test("Inverting with non-invertible sections", () => { - const formula = Formula.add(variable, constant.ceil()); - expect(formula.isInvertible()).toBe(true); - expect(formula.invert(10)).compare_tolerance(0); + describe("Inverting with non-invertible sections", () => { + test("Non-invertible constant", () => { + const formula = Formula.add(variable, constant.ceil()); + expect(formula.isInvertible()).toBe(true); + expect(() => formula.invert(10)).not.toThrow(); + }); + test("Non-invertible variable", () => { + const formula = Formula.add(variable.ceil(), constant); + expect(formula.isInvertible()).toBe(false); + expect(() => formula.invert(10)).toThrow(); + }); }); }); @@ -619,6 +626,19 @@ describe("Integrating", () => { const formula = Formula.pow(1.05, variable).times(100).pow(0.5); expect(() => formula.evaluateIntegral()).toThrow(); }); + + describe("Integrating with non-integrable sections", () => { + test("Non-integrable constant", () => { + const formula = Formula.add(variable, constant.ceil()); + expect(formula.isIntegrable()).toBe(true); + expect(() => formula.evaluateIntegral()).not.toThrow(); + }); + test("Non-integrable variable", () => { + const formula = Formula.add(variable.ceil(), constant); + expect(formula.isIntegrable()).toBe(false); + expect(() => formula.evaluateIntegral()).toThrow(); + }); + }); }); describe("Inverting integrals", () => { From 5d17d67e00bc063001413f621f7bc068eeae92fd Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 30 Apr 2023 11:23:38 -0500 Subject: [PATCH 09/83] Fix repeatable amount display --- src/features/repeatable.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index d9bf372..eb34af5 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -235,12 +235,10 @@ export function createRepeatable( {currDisplay.showAmount === false ? null : (

- joinJSX( - <>Amount: {formatWhole(genericRepeatable.amount.value)}, - {unref(genericRepeatable.limit) !== Decimal.dInf ? ( + <>Amount: {formatWhole(genericRepeatable.amount.value)} + {Decimal.isFinite(unref(genericRepeatable.limit)) ? ( <> / {formatWhole(unref(genericRepeatable.limit))} ) : undefined} - )
)} {currDisplay.effectDisplay == null ? null : ( From 04f14c17bd6939fe6460c2697a46d263ff1d4890 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 30 Apr 2023 11:37:23 -0500 Subject: [PATCH 10/83] Fix `extends undefined` checks --- src/features/tooltips/tooltip.ts | 2 +- src/game/modifiers.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts index 129bd16..b5b1042 100644 --- a/src/features/tooltips/tooltip.ts +++ b/src/features/tooltips/tooltip.ts @@ -52,7 +52,7 @@ export interface BaseTooltip { export type Tooltip = Replace< T & BaseTooltip, { - pinnable: T["pinnable"] extends undefined ? false : T["pinnable"]; + pinnable: undefined extends T["pinnable"] ? false : T["pinnable"]; pinned: T["pinnable"] extends true ? Ref : undefined; display: GetComputableType; classes: GetComputableType; diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index cceacd8..b65e7fc 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -41,11 +41,11 @@ export interface Modifier { /** * Utility type used to narrow down a modifier type that will have a description and/or enabled property based on optional parameters, T and S (respectively). */ -export type ModifierFromOptionalParams = T extends undefined - ? S extends undefined +export type ModifierFromOptionalParams = undefined extends T + ? undefined extends S ? Omit, "description" | "enabled"> : Omit, "description"> - : S extends undefined + : undefined extends S ? Omit, "enabled"> : WithRequired; From 8dd2cbe46680ea20a73571af11d8e1a7ded6d880 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 30 Apr 2023 13:17:04 -0500 Subject: [PATCH 11/83] Make upgrade.canPurchase return false when already bought --- src/features/upgrades/upgrade.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 3861c5e..9ff9b28 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -137,7 +137,9 @@ export function createUpgrade( upgrade.bought = bought; Object.assign(upgrade, decoratedData); - upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements)); + upgrade.canPurchase = computed( + () => !bought.value && requirementsMet(upgrade.requirements) + ); upgrade.purchase = function () { const genericUpgrade = upgrade as GenericUpgrade; if (!unref(genericUpgrade.canPurchase)) { From dbdcf19b6d0f2c041ac60570247f91cce0307727 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 1 May 2023 08:20:30 -0500 Subject: [PATCH 12/83] Fix actions not being constructible --- src/features/action.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/action.tsx b/src/features/action.tsx index a58a762..7d392bb 100644 --- a/src/features/action.tsx +++ b/src/features/action.tsx @@ -244,8 +244,9 @@ export function createAction( decorator.postConstruct?.(action); } - const decoratedProps = decorators.reduce((current, next) => - Object.assign(current, next.getGatheredProps?.(action)) + const decoratedProps = decorators.reduce( + (current, next) => Object.assign(current, next.getGatheredProps?.(action)), + {} ); action[GatherProps] = function (this: GenericAction) { const { From 3413585c45c1c3cc32975fd67e6ce90ff2974db2 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 1 May 2023 08:32:22 -0500 Subject: [PATCH 13/83] Fix bar misalignment on actions --- src/features/action.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/action.tsx b/src/features/action.tsx index 7d392bb..1fbb8d3 100644 --- a/src/features/action.tsx +++ b/src/features/action.tsx @@ -169,7 +169,6 @@ export function createAction( direction: Direction.Right, width: 100, height: 10, - style: "margin-top: 8px", borderStyle: "border-color: black", baseStyle: "margin-top: -1px", progress: () => Decimal.div(progress.value, unref(genericAction.duration)), From 6786c27b8931f6cebd94b7f93726ddc066573f96 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 3 May 2023 15:50:51 -0500 Subject: [PATCH 14/83] Remove lint from readme --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index e535eb2..5f8c741 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,6 @@ npm run build npm run preview ``` -### Lints and fixes files -``` -npm run lint -``` - ### Runs the tests using vite-jest ``` npm run test From 4d7f03d543b00e5903f5f2d507e0840acd6fd920 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Fri, 5 May 2023 19:10:23 -0500 Subject: [PATCH 15/83] Fix crash when calculating formula cost Happened when spend resource was false and the formula was non-integrable, but the amount to buy were all going to be summed anyways --- src/game/formulas/formulas.ts | 40 ++++++++++++++++++----------------- tests/game/formulas.test.ts | 21 ++++++++++++++++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 79adfce..76d8ff9 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1491,33 +1491,35 @@ export function calculateCost( spendResources = true, summedPurchases?: number ) { - let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0); + const origValue = unref(formula.innermostVariable) ?? 0; + let newValue = Decimal.add(amountToBuy, origValue); + const targetValue = newValue; + summedPurchases ??= spendResources ? 10 : 0; + newValue = newValue.sub(summedPurchases).clampMin(origValue); + let cost: DecimalSource = 0; if (spendResources) { - if (!formula.isIntegrable()) { - throw new Error( - "Cannot calculate cost with spending resources of non-integrable formula" - ); + if (Decimal.gt(amountToBuy, summedPurchases)) { + if (!formula.isIntegrable()) { + throw new Error( + "Cannot calculate cost with spending resources of non-integrable formula" + ); + } + cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); } - const targetValue = newValue; - newValue = newValue - .sub(summedPurchases ?? 10) - .clampMin(unref(formula.innermostVariable) ?? 0); - let cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); if (targetValue.gt(1e308)) { // Too large of a number for summedPurchases to make a difference, // just get the cost and multiply by summed purchases - return cost.add(Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue))); + return Decimal.add( + cost, + Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue)) + ); } for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) { - cost = cost.add(formula.evaluate(i)); + cost = Decimal.add(cost, formula.evaluate(i)); } - return cost; } else { - const targetValue = newValue; - newValue = newValue - .sub(summedPurchases ?? 0) - .clampMin(unref(formula.innermostVariable) ?? 0); - let cost = formula.evaluate(newValue); + cost = formula.evaluate(newValue); + newValue = newValue.add(1); if (targetValue.gt(1e308)) { // Too large of a number for summedPurchases to make a difference, // just get the cost and multiply by summed purchases @@ -1526,6 +1528,6 @@ export function calculateCost( for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) { cost = Decimal.add(cost, formula.evaluate(i)); } - return cost; } + return cost; } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 462d3d8..c0c067a 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1089,9 +1089,21 @@ describe("Buy Max", () => { Decimal.pow(1.05, 141).times(100) ); }); + test("Calculates max affordable and cost correctly with summing last purchases", () => { + const variable = Formula.variable(0); + const formula = Formula.pow(1.05, variable).times(100); + const maxAffordable = calculateMaxAffordable(formula, resource, false, 4); + expect(maxAffordable.value).compare_tolerance(141 - 4); + + const actualCost = new Array(4) + .fill(null) + .reduce((acc, _, i) => acc.add(formula.evaluate(133 + i)), new Decimal(0)); + const calculatedCost = calculateCost(formula, maxAffordable.value, false, 4); + expect(calculatedCost).compare_tolerance(actualCost); + }); }); describe("With spending", () => { - test("Throws on non-invertible formula", () => { + test("Throws on calculating max affordable of non-invertible formula", () => { const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); @@ -1220,7 +1232,7 @@ describe("Buy Max", () => { (acc, _, i) => acc.add(formula.evaluate(i + purchases.value)), new Decimal(0) ); - const calculatedCost = calculateCost(formula, maxAffordable.value, true); + const calculatedCost = calculateCost(formula, maxAffordable.value); // Since we're summing all the purchases this should be equivalent expect(calculatedCost).compare_tolerance(actualCost); }); @@ -1235,5 +1247,10 @@ describe("Buy Max", () => { expect(Decimal.isFinite(calculatedCost)).toBe(true); resource.value = 100000; }); + test("Handles summing purchases of non-integrable formula", () => { + const purchases = ref(0); + const formula = Formula.variable(purchases).abs(); + expect(() => calculateCost(formula, 10)).not.toThrow(); + }); }); }); From 0e1915f5118c397bc518ce53a407bedacb633d82 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 7 May 2023 21:51:11 -0500 Subject: [PATCH 16/83] Fix conversion utility showing currentAt instead of nextAt --- src/data/common.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index 8cc7853..e7a9db0 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -134,9 +134,9 @@ export function createResetButton From cb4830e06b028a4d363ff5c9696e00c7a93a81ee Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 10 May 2023 00:33:45 -0500 Subject: [PATCH 17/83] Fix reset typing --- src/features/reset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/reset.ts b/src/features/reset.ts index b7c40ed..2811ba8 100644 --- a/src/features/reset.ts +++ b/src/features/reset.ts @@ -19,7 +19,7 @@ export const ResetType = Symbol("Reset"); */ export interface ResetOptions { /** List of things to reset. Can include objects which will be recursed over for persistent values. */ - thingsToReset: Computable[]>; + thingsToReset: Computable; /** A function that is called when the reset is performed. */ onReset?: VoidFunction; } From 866685de2db5bf9ae00f23541d2d9aecafbcc872 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 11 May 2023 23:32:10 -0500 Subject: [PATCH 18/83] Simplify TPS --- src/components/TPS.vue | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/components/TPS.vue b/src/components/TPS.vue index a460a09..db73da6 100644 --- a/src/components/TPS.vue +++ b/src/components/TPS.vue @@ -1,17 +1,11 @@ diff --git a/src/components/Error.vue b/src/components/Error.vue new file mode 100644 index 0000000..19b6fe4 --- /dev/null +++ b/src/components/Error.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/components/Layer.vue b/src/components/Layer.vue index c00cf8a..1857535 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -1,5 +1,6 @@