Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve lazy proxy typing #16

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/data/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
198 changes: 97 additions & 101 deletions src/features/achievements/achievement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
GatherProps,
GenericComponent,
OptionsFunc,
Replace,
StyleValue,
Visibility,
getUniqueID,
Expand All @@ -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, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";

const toast = useToast();
Expand Down Expand Up @@ -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<Visibility | boolean>;
/** Whether or not this achievement has been earned. */
earned: Persistent<boolean>;
/** A function to complete this achievement. */
Expand All @@ -110,35 +105,20 @@ 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 type Achievement<T extends AchievementOptions> = BaseAchievement &
ProcessedFeature<AchievementOptions, Exclude<T, BaseAchievement>> &
Defaults<
Exclude<T, BaseAchievement>,
{
showPopups: true;
}
>;

/** A type that matches any valid {@link Achievement} object. */
export type GenericAchievement = Replace<
Achievement<AchievementOptions>,
{
visibility: ProcessedComputable<Visibility | boolean>;
showPopups: ProcessedComputable<boolean>;
}
>;
// 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, BaseAchievement, GenericAchievement>,
optionsFunc?: OptionsFunc<T, GenericAchievement>,
...decorators: GenericDecorator[]
): Achievement<T> {
const earned = persistent<boolean>(false, false);
Expand All @@ -147,59 +127,17 @@ export function createAchievement<T extends AchievementOptions>(
{}
);
return createLazyProxy(feature => {
const achievement =
const { visibility, display, mark, small, image, style, classes, showPopups, ...options } =
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.earned = earned;
achievement.complete = function () {
earned.value = true;
const genericAchievement = achievement as GenericAchievement;
genericAchievement.onComplete?.();
if (
genericAchievement.display != null &&
unref(genericAchievement.showPopups) === true
) {
const display = unref(genericAchievement.display);
let Display;
if (isCoercableComponent(display)) {
Display = coerceComponent(display);
} else if (display.requirement != null) {
Display = coerceComponent(display.requirement);
} else {
Display = displayRequirements(genericAchievement.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);

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);
const optionsVisibility = convertComputable(visibility, feature) ?? Visibility.Visible;
const processedVisibility = 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) &&
Expand All @@ -211,35 +149,74 @@ export function createAchievement<T extends AchievementOptions>(
) {
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);
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);
for (const decorator of decorators) {
decorator.postConstruct?.(achievement);
}

const decoratedProps = decorators.reduce(
(current, next) => Object.assign(current, next.getGatheredProps?.(achievement)),
{}
);
achievement[GatherProps] = function (this: GenericAchievement) {

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>
);
}
}

function gatherProps(this: GenericAchievement): Record<string, unknown> {
const {
visibility,
display,
Expand All @@ -265,29 +242,48 @@ export function createAchievement<T extends AchievementOptions>(
id,
...decoratedProps
};
};
}

if (achievement.requirements) {
const genericAchievement = achievement as GenericAchievement;
if (achievement.requirements != null) {
const requirements = [
createVisibilityRequirement(genericAchievement),
createBooleanRequirement(() => !genericAchievement.earned.value),
createVisibilityRequirement(achievement),
createBooleanRequirement(() => !earned.value),
...(isArray(achievement.requirements)
? achievement.requirements
: [achievement.requirements])
];
watchEffect(() => {
if (settings.active !== player.id) return;
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" {
interface Settings {
msDisplay: AchievementDisplay;
Expand Down
4 changes: 2 additions & 2 deletions src/features/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* 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;
/**
Expand Down
Loading