Made achievements use requirements system, and document them

This commit is contained in:
thepaperpilot 2023-04-02 23:49:51 -05:00
parent e6c7ad62a7
commit 742d2293d0

View file

@ -1,3 +1,4 @@
import { isArray } from "@vue/shared";
import AchievementComponent from "features/achievements/Achievement.vue";
import {
CoercableComponent,
@ -5,7 +6,6 @@ import {
GatherProps,
GenericComponent,
getUniqueID,
isVisible,
OptionsFunc,
Replace,
setDefault,
@ -16,6 +16,12 @@ import "game/notifications";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import player from "game/player";
import {
createBooleanRequirement,
createVisibilityRequirement,
Requirements,
requirementsMet
} from "game/requirements";
import settings from "game/settings";
import type {
Computable,
@ -31,28 +37,50 @@ import { useToast } from "vue-toastification";
const toast = useToast();
/** A symbol used to identify {@link Achievement} features. */
export const AchievementType = Symbol("Achievement");
/**
* An object that configures an {@link Achievement}.
*/
export interface AchievementOptions {
/** Whether this achievement should be visible. */
visibility?: Computable<Visibility | boolean>;
shouldEarn?: () => boolean;
/** The requirement(s) to earn this achievement. Can be left null if using {@link BaseAchievement.complete}. */
requirements?: Requirements;
/** The display to use for this achievement. */
display?: Computable<CoercableComponent>;
/** Shows a marker on the corner of the feature. */
mark?: Computable<boolean | string>;
/** An image to display as the background for this achievement. */
image?: Computable<string>;
/** CSS to apply to this feature. */
style?: Computable<StyleValue>;
/** Dictionary of CSS classes to apply to this feature. */
classes?: Computable<Record<string, boolean>>;
/** A function that is called when the achievement is completed. */
onComplete?: VoidFunction;
}
/**
* The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
*/
export interface BaseAchievement {
/** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */
id: string;
/** Whether or not this achievement has been earned. */
earned: Persistent<boolean>;
/** A function to complete this achievement. */
complete: VoidFunction;
/** A symbol that helps identify features of the same type. */
type: typeof AchievementType;
/** The Vue component used to render this feature. */
[Component]: GenericComponent;
/** A function to gather the props the vue component requires for this feature. */
[GatherProps]: () => Record<string, unknown>;
}
/** An object that represents a feature with that is passively earned upon meeting certain requirements. */
export type Achievement<T extends AchievementOptions> = Replace<
T & BaseAchievement,
{
@ -65,6 +93,7 @@ export type Achievement<T extends AchievementOptions> = Replace<
}
>;
/** A type that matches any valid {@link Achievement} object. */
export type GenericAchievement = Replace<
Achievement<AchievementOptions>,
{
@ -72,6 +101,10 @@ export type GenericAchievement = Replace<
}
>;
/**
* Lazily creates a achievement with the given options.
* @param optionsFunc Achievement options.
*/
export function createAchievement<T extends AchievementOptions>(
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
): Achievement<T> {
@ -85,6 +118,21 @@ export function createAchievement<T extends AchievementOptions>(
achievement.earned = earned;
achievement.complete = function () {
earned.value = true;
const genericAchievement = achievement as GenericAchievement;
genericAchievement.onComplete?.();
if (genericAchievement.display != null) {
const Display = coerceComponent(unref(genericAchievement.display));
toast.info(
<div>
<h3>Achievement earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</div>
);
}
};
processComputable(achievement as T, "visibility");
@ -100,30 +148,19 @@ export function createAchievement<T extends AchievementOptions>(
return { visibility, display, earned, image, style: unref(style), classes, mark, id };
};
if (achievement.shouldEarn) {
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 (
!genericAchievement.earned.value &&
isVisible(genericAchievement.visibility) &&
genericAchievement.shouldEarn?.()
) {
genericAchievement.earned.value = true;
genericAchievement.onComplete?.();
if (genericAchievement.display != null) {
const Display = coerceComponent(unref(genericAchievement.display));
toast.info(
<div>
<h3>Achievement earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</div>
);
}
if (requirementsMet(requirements)) {
genericAchievement.complete();
}
});
}