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