[WIP] Improve lazy proxy typing #16

Draft
thepaperpilot wants to merge 4 commits from fix/lazy-proxy-typing into main
Showing only changes of commit cf069cee61 - Show all commits

View file

@ -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<string, unknown>;
}
/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */
// export type Achievement<T extends AchievementOptions> = Replace<
// T & BaseAchievement,
// {
// visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
// 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>;
// }
// >;
// export interface Achievement extends AchievementOptions, BaseAchievement {
// visibility: GetComputableTypeWithDefault<AchievementOptions["visibility"], Visibility.Visible>;
// display: GetComputableType<AchievementOptions["display"]>;
// mark: GetComputableType<AchievementOptions["mark"]>;
// image: GetComputableType<AchievementOptions["image"]>;
// style: GetComputableType<AchievementOptions["style"]>;
// classes: GetComputableType<AchievementOptions["classes"]>;
// showPopups: GetComputableTypeWithDefault<AchievementOptions["showPopups"], true>;
// }
export type Achievement<T extends AchievementOptions> = BaseAchievement &
ProcessedFeature<AchievementOptions, Exclude<T, BaseAchievement>> &
Defaults<
@ -141,154 +117,6 @@ export type Achievement<T extends AchievementOptions> = BaseAchievement &
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GenericAchievement = Achievement<any>;
/**
* Lazily creates an achievement with the given options.
* @param optionsFunc Achievement options.
*/
// export function createAchievement<T extends AchievementOptions>(
// optionsFunc?: OptionsFunc<T, GenericAchievement>,
// ...decorators: GenericDecorator[]
// ): Achievement<T> {
// const earned = persistent<boolean>(false, false);
// const decoratedData = decorators.reduce(
// (current, next) => Object.assign(current, next.getPersistentData?.()),
// {}
// );
// return createLazyProxy(feature => {
// const achievement =
// optionsFunc?.call(feature, feature) ??
// ({} as ReturnType<NonNullable<typeof optionsFunc>>);
// 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(
// <div>
// <h3>Achievement earned!</h3>
// <div>
// {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
// {/* @ts-ignore */}
// <Display />
// </div>
// </div>
// );
// }
// };
// 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<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;
// }
// });
// 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<T extends AchievementOptions>(
optionsFunc?: OptionsFunc<T, GenericAchievement>,
...decorators: GenericDecorator[]
@ -298,42 +126,13 @@ export function createAchievement<T extends AchievementOptions>(
(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<NonNullable<typeof optionsFunc>>);
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(
<div>
<h3>Achievement earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</div>
);
}
}
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<T extends AchievementOptions>(
}
});
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) {
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(
<div>
<h3>Achievement earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</div>
);
}
}
const decoratedProps = decorators.reduce(
(current, next) => Object.assign(current, next.getGatheredProps?.(achievement)),
{}
);
function gatherProps(this: Achievement<T>) {
function gatherProps(this: Achievement<T>): Record<string, unknown> {
const {
visibility,
display,
@ -399,46 +242,27 @@ export function createAchievement<T extends AchievementOptions>(
};
}
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<T>*/;
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<Visibility | boolean>
const badAch = createAchievement(() => ({
requirements: "foo"
}));
declare module "game/settings" {
interface Settings {
msDisplay: AchievementDisplay;