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 AchievementComponent from "features/achievements/Achievement.vue";
import { import {
CoercableComponent, CoercableComponent,
@ -5,7 +6,6 @@ import {
GatherProps, GatherProps,
GenericComponent, GenericComponent,
getUniqueID, getUniqueID,
isVisible,
OptionsFunc, OptionsFunc,
Replace, Replace,
setDefault, setDefault,
@ -16,6 +16,12 @@ import "game/notifications";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence"; import { persistent } from "game/persistence";
import player from "game/player"; import player from "game/player";
import {
createBooleanRequirement,
createVisibilityRequirement,
Requirements,
requirementsMet
} from "game/requirements";
import settings from "game/settings"; import settings from "game/settings";
import type { import type {
Computable, Computable,
@ -31,28 +37,50 @@ import { useToast } from "vue-toastification";
const toast = useToast(); const toast = useToast();
/** A symbol used to identify {@link Achievement} features. */
export const AchievementType = Symbol("Achievement"); export const AchievementType = Symbol("Achievement");
/**
* An object that configures an {@link Achievement}.
*/
export interface AchievementOptions { export interface AchievementOptions {
/** Whether this achievement should be visible. */
visibility?: Computable<Visibility | boolean>; 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>; display?: Computable<CoercableComponent>;
/** Shows a marker on the corner of the feature. */
mark?: Computable<boolean | string>; mark?: Computable<boolean | string>;
/** An image to display as the background for this achievement. */
image?: Computable<string>; image?: Computable<string>;
/** CSS to apply to this feature. */
style?: Computable<StyleValue>; style?: Computable<StyleValue>;
/** Dictionary of CSS classes to apply to this feature. */
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
/** A function that is called when the achievement is completed. */
onComplete?: VoidFunction; onComplete?: VoidFunction;
} }
/**
* The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
*/
export interface BaseAchievement { export interface BaseAchievement {
/** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */
id: string; id: string;
/** Whether or not this achievement has been earned. */
earned: Persistent<boolean>; earned: Persistent<boolean>;
/** A function to complete this achievement. */
complete: VoidFunction; complete: VoidFunction;
/** A symbol that helps identify features of the same type. */
type: typeof AchievementType; type: typeof AchievementType;
/** The Vue component used to render this feature. */
[Component]: GenericComponent; [Component]: GenericComponent;
/** A function to gather the props the vue component requires for this feature. */
[GatherProps]: () => Record<string, unknown>; [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< export type Achievement<T extends AchievementOptions> = Replace<
T & BaseAchievement, 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< export type GenericAchievement = Replace<
Achievement<AchievementOptions>, 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>( export function createAchievement<T extends AchievementOptions>(
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement> optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
): Achievement<T> { ): Achievement<T> {
@ -85,6 +118,21 @@ export function createAchievement<T extends AchievementOptions>(
achievement.earned = earned; achievement.earned = earned;
achievement.complete = function () { achievement.complete = function () {
earned.value = true; 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"); 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 }; return { visibility, display, earned, image, style: unref(style), classes, mark, id };
}; };
if (achievement.shouldEarn) { if (achievement.requirements) {
const genericAchievement = achievement as GenericAchievement; const genericAchievement = achievement as GenericAchievement;
const requirements = [
createVisibilityRequirement(genericAchievement),
createBooleanRequirement(() => !genericAchievement.earned.value),
...(isArray(achievement.requirements)
? achievement.requirements
: [achievement.requirements])
];
watchEffect(() => { watchEffect(() => {
if (settings.active !== player.id) return; if (settings.active !== player.id) return;
if ( if (requirementsMet(requirements)) {
!genericAchievement.earned.value && genericAchievement.complete();
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>
);
}
} }
}); });
} }