Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
26772e832e
7 changed files with 350 additions and 426 deletions
|
@ -1,10 +1,10 @@
|
||||||
import Collapsible from "components/layout/Collapsible.vue";
|
import Collapsible from "components/layout/Collapsible.vue";
|
||||||
|
import { GenericAchievement } from "features/achievements/achievement";
|
||||||
import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable";
|
import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable";
|
||||||
import { createClickable } from "features/clickables/clickable";
|
import { createClickable } from "features/clickables/clickable";
|
||||||
import type { GenericConversion } from "features/conversion";
|
import type { GenericConversion } from "features/conversion";
|
||||||
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
||||||
import { jsx, setDefault } from "features/feature";
|
import { jsx, setDefault } from "features/feature";
|
||||||
import { GenericMilestone } from "features/milestones/milestone";
|
|
||||||
import { displayResource, Resource } from "features/resources/resource";
|
import { displayResource, Resource } from "features/resources/resource";
|
||||||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||||
import { createTreeNode } from "features/trees/tree";
|
import { createTreeNode } from "features/trees/tree";
|
||||||
|
@ -384,35 +384,35 @@ export function colorText(textToColor: string, color = "var(--accent2)"): JSX.El
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a collapsible display of a list of milestones
|
* Creates a collapsible display of a list of achievements
|
||||||
* @param milestones A dictionary of the milestones to display, inserted in the order from easiest to hardest
|
* @param achievements A dictionary of the achievements to display, inserted in the order from easiest to hardest
|
||||||
*/
|
*/
|
||||||
export function createCollapsibleMilestones(milestones: Record<string, GenericMilestone>) {
|
export function createCollapsibleAchievements(achievements: Record<string, GenericAchievement>) {
|
||||||
// Milestones are typically defined from easiest to hardest, and we want to show hardest first
|
// Achievements are typically defined from easiest to hardest, and we want to show hardest first
|
||||||
const orderedMilestones = Object.values(milestones).reverse();
|
const orderedAchievements = Object.values(achievements).reverse();
|
||||||
const collapseMilestones = persistent<boolean>(true, false);
|
const collapseAchievements = persistent<boolean>(true, false);
|
||||||
const lockedMilestones = computed(() =>
|
const lockedAchievements = computed(() =>
|
||||||
orderedMilestones.filter(m => m.earned.value === false)
|
orderedAchievements.filter(m => m.earned.value === false)
|
||||||
);
|
);
|
||||||
const { firstFeature, collapsedContent, hasCollapsedContent } = getFirstFeature(
|
const { firstFeature, collapsedContent, hasCollapsedContent } = getFirstFeature(
|
||||||
orderedMilestones,
|
orderedAchievements,
|
||||||
m => m.earned.value
|
m => m.earned.value
|
||||||
);
|
);
|
||||||
const display = jsx(() => {
|
const display = jsx(() => {
|
||||||
const milestonesToDisplay = [...lockedMilestones.value];
|
const achievementsToDisplay = [...lockedAchievements.value];
|
||||||
if (firstFeature.value) {
|
if (firstFeature.value) {
|
||||||
milestonesToDisplay.push(firstFeature.value);
|
achievementsToDisplay.push(firstFeature.value);
|
||||||
}
|
}
|
||||||
return renderColJSX(
|
return renderColJSX(
|
||||||
...milestonesToDisplay,
|
...achievementsToDisplay,
|
||||||
jsx(() => (
|
jsx(() => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
collapsed={collapseMilestones}
|
collapsed={collapseAchievements}
|
||||||
content={collapsedContent}
|
content={collapsedContent}
|
||||||
display={
|
display={
|
||||||
collapseMilestones.value
|
collapseAchievements.value
|
||||||
? "Show other completed milestones"
|
? "Show other completed achievements"
|
||||||
: "Hide other completed milestones"
|
: "Hide other completed achievements"
|
||||||
}
|
}
|
||||||
v-show={unref(hasCollapsedContent)}
|
v-show={unref(hasCollapsedContent)}
|
||||||
/>
|
/>
|
||||||
|
@ -420,7 +420,7 @@ export function createCollapsibleMilestones(milestones: Record<string, GenericMi
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
collapseMilestones,
|
collapseAchievements: collapseAchievements,
|
||||||
display
|
display
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,24 +13,27 @@
|
||||||
achievement: true,
|
achievement: true,
|
||||||
locked: !unref(earned),
|
locked: !unref(earned),
|
||||||
bought: unref(earned),
|
bought: unref(earned),
|
||||||
|
small: unref(small),
|
||||||
...unref(classes)
|
...unref(classes)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<component v-if="component" :is="component" />
|
<component v-if="comp" :is="comp" />
|
||||||
<MarkNode :mark="unref(mark)" />
|
<MarkNode :mark="unref(mark)" />
|
||||||
<Node :id="id" />
|
<Node :id="id" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="tsx">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import { CoercableComponent, jsx } from "features/feature";
|
||||||
import { Visibility, isHidden, isVisible } from "features/feature";
|
import { Visibility, isHidden, isVisible } from "features/feature";
|
||||||
import { computeOptionalComponent, processedPropType } from "util/vue";
|
import { displayRequirements, Requirements } from "game/requirements";
|
||||||
import type { StyleValue } from "vue";
|
import { coerceComponent, computeOptionalComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
||||||
|
import { Component, shallowRef, StyleValue, UnwrapRef, watchEffect } from "vue";
|
||||||
import { defineComponent, toRefs, unref } from "vue";
|
import { defineComponent, toRefs, unref } from "vue";
|
||||||
|
import { GenericAchievement } from "./achievement";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -38,15 +41,17 @@ export default defineComponent({
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
display: processedPropType<UnwrapRef<GenericAchievement["display"]>>(Object, String, Function),
|
||||||
earned: {
|
earned: {
|
||||||
type: processedPropType<boolean>(Boolean),
|
type: processedPropType<boolean>(Boolean),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
requirements: processedPropType<Requirements>(Object, Array),
|
||||||
image: processedPropType<string>(String),
|
image: processedPropType<string>(String),
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
style: processedPropType<StyleValue>(String, Object, Array),
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
classes: processedPropType<Record<string, boolean>>(Object),
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
mark: processedPropType<boolean | string>(Boolean, String),
|
||||||
|
small: processedPropType<boolean>(Boolean),
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
@ -57,10 +62,44 @@ export default defineComponent({
|
||||||
MarkNode
|
MarkNode
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { display } = toRefs(props);
|
const { display, requirements } = toRefs(props);
|
||||||
|
|
||||||
|
const comp = shallowRef<Component | string>("");
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const currDisplay = unwrapRef(display);
|
||||||
|
if (currDisplay == null) {
|
||||||
|
comp.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isCoercableComponent(currDisplay)) {
|
||||||
|
comp.value = coerceComponent(currDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Requirement = currDisplay.requirement ? coerceComponent(currDisplay.requirement, "h3") : displayRequirements(unwrapRef(requirements) ?? []);
|
||||||
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
|
||||||
|
const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
|
||||||
|
comp.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<span>
|
||||||
|
<Requirement />
|
||||||
|
{currDisplay.effectDisplay != null ? (
|
||||||
|
<div>
|
||||||
|
<EffectDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{currDisplay.optionsDisplay != null ? (
|
||||||
|
<div class="equal-spaced">
|
||||||
|
<OptionsDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
component: computeOptionalComponent(display),
|
comp,
|
||||||
unref,
|
unref,
|
||||||
Visibility,
|
Visibility,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
@ -78,4 +117,31 @@ export default defineComponent({
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 0 0 2px #000000;
|
text-shadow: 0 0 2px #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.achievement:not(.small) {
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
min-width: 120px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
background-color: var(--locked);
|
||||||
|
border-width: 4px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: unset;
|
||||||
|
text-shadow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement.done {
|
||||||
|
background-color: var(--bought);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement :deep(.equal-spaced) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement :deep(.equal-spaced > *) {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { computed } from "@vue/reactivity";
|
||||||
|
import { isArray } from "@vue/shared";
|
||||||
|
import Select from "components/fields/Select.vue";
|
||||||
import AchievementComponent from "features/achievements/Achievement.vue";
|
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||||
import {
|
import {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
|
@ -5,18 +8,27 @@ import {
|
||||||
GatherProps,
|
GatherProps,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
getUniqueID,
|
getUniqueID,
|
||||||
isVisible,
|
jsx,
|
||||||
OptionsFunc,
|
OptionsFunc,
|
||||||
Replace,
|
Replace,
|
||||||
setDefault,
|
setDefault,
|
||||||
StyleValue,
|
StyleValue,
|
||||||
Visibility
|
Visibility
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
|
import { globalBus } from "game/events";
|
||||||
import "game/notifications";
|
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 settings from "game/settings";
|
import {
|
||||||
|
createBooleanRequirement,
|
||||||
|
createVisibilityRequirement,
|
||||||
|
displayRequirements,
|
||||||
|
Requirements,
|
||||||
|
requirementsMet
|
||||||
|
} from "game/requirements";
|
||||||
|
import settings, { registerSettingField } from "game/settings";
|
||||||
|
import { camelToTitle } from "util/common";
|
||||||
import type {
|
import type {
|
||||||
Computable,
|
Computable,
|
||||||
GetComputableType,
|
GetComputableType,
|
||||||
|
@ -25,34 +37,79 @@ import type {
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
import { processComputable } from "util/computed";
|
import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { coerceComponent } from "util/vue";
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||||
import { unref, watchEffect } from "vue";
|
import { unref, watchEffect } from "vue";
|
||||||
import { useToast } from "vue-toastification";
|
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");
|
||||||
|
|
||||||
|
/** Modes for only displaying some achievements. */
|
||||||
|
export enum AchievementDisplay {
|
||||||
|
All = "all",
|
||||||
|
//Last = "last",
|
||||||
|
Configurable = "configurable",
|
||||||
|
Incomplete = "incomplete",
|
||||||
|
None = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}. */
|
||||||
display?: Computable<CoercableComponent>;
|
requirements?: Requirements;
|
||||||
|
/** The display to use for this achievement. */
|
||||||
|
display?: Computable<
|
||||||
|
| CoercableComponent
|
||||||
|
| {
|
||||||
|
/** Description of the requirement(s) for this achievement. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
|
||||||
|
requirement?: CoercableComponent;
|
||||||
|
/** Description of what will change (if anything) for achieving this. */
|
||||||
|
effectDisplay?: CoercableComponent;
|
||||||
|
/** Any additional things to display on this achievement, such as a toggle for it's effect. */
|
||||||
|
optionsDisplay?: CoercableComponent;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
|
/** Toggles a smaller design for the feature. */
|
||||||
|
small?: Computable<boolean>;
|
||||||
|
/** 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>>;
|
||||||
|
/** Whether or not to display a notification popup when this achievement is earned. */
|
||||||
|
showPopups?: Computable<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,
|
||||||
{
|
{
|
||||||
|
@ -62,16 +119,23 @@ export type Achievement<T extends AchievementOptions> = Replace<
|
||||||
image: GetComputableType<T["image"]>;
|
image: GetComputableType<T["image"]>;
|
||||||
style: GetComputableType<T["style"]>;
|
style: GetComputableType<T["style"]>;
|
||||||
classes: GetComputableType<T["classes"]>;
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
showPopups: GetComputableTypeWithDefault<T["showPopups"], true>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Achievement} object. */
|
||||||
export type GenericAchievement = Replace<
|
export type GenericAchievement = Replace<
|
||||||
Achievement<AchievementOptions>,
|
Achievement<AchievementOptions>,
|
||||||
{
|
{
|
||||||
visibility: ProcessedComputable<Visibility | boolean>;
|
visibility: ProcessedComputable<Visibility | boolean>;
|
||||||
|
showPopups: ProcessedComputable<boolean>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,45 +149,114 @@ 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 &&
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
processComputable(achievement as T, "visibility");
|
processComputable(achievement as T, "visibility");
|
||||||
setDefault(achievement, "visibility", Visibility.Visible);
|
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, "display");
|
||||||
processComputable(achievement as T, "mark");
|
processComputable(achievement as T, "mark");
|
||||||
|
processComputable(achievement as T, "small");
|
||||||
processComputable(achievement as T, "image");
|
processComputable(achievement as T, "image");
|
||||||
processComputable(achievement as T, "style");
|
processComputable(achievement as T, "style");
|
||||||
processComputable(achievement as T, "classes");
|
processComputable(achievement as T, "classes");
|
||||||
|
processComputable(achievement as T, "showPopups");
|
||||||
|
setDefault(achievement, "showPopups", true);
|
||||||
|
|
||||||
achievement[GatherProps] = function (this: GenericAchievement) {
|
achievement[GatherProps] = function (this: GenericAchievement) {
|
||||||
const { visibility, display, earned, image, style, classes, mark, id } = this;
|
const {
|
||||||
return { visibility, display, earned, image, style: unref(style), classes, mark, id };
|
visibility,
|
||||||
|
display,
|
||||||
|
requirements,
|
||||||
|
earned,
|
||||||
|
image,
|
||||||
|
style,
|
||||||
|
classes,
|
||||||
|
mark,
|
||||||
|
small,
|
||||||
|
id
|
||||||
|
} = this;
|
||||||
|
return {
|
||||||
|
visibility,
|
||||||
|
display,
|
||||||
|
requirements,
|
||||||
|
earned,
|
||||||
|
image,
|
||||||
|
style: unref(style),
|
||||||
|
classes,
|
||||||
|
mark,
|
||||||
|
small,
|
||||||
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -131,3 +264,34 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
return achievement as unknown as Achievement<T>;
|
return achievement as unknown as Achievement<T>;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "game/settings" {
|
||||||
|
interface Settings {
|
||||||
|
msDisplay: AchievementDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalBus.on("loadSettings", settings => {
|
||||||
|
setDefault(settings, "msDisplay", AchievementDisplay.All);
|
||||||
|
});
|
||||||
|
|
||||||
|
const msDisplayOptions = Object.values(AchievementDisplay).map(option => ({
|
||||||
|
label: camelToTitle(option),
|
||||||
|
value: option
|
||||||
|
}));
|
||||||
|
|
||||||
|
registerSettingField(
|
||||||
|
jsx(() => (
|
||||||
|
<Select
|
||||||
|
title={jsx(() => (
|
||||||
|
<span class="option-title">
|
||||||
|
Show achievements
|
||||||
|
<desc>Select which achievements to display based on criterias.</desc>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
options={msDisplayOptions}
|
||||||
|
onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
|
||||||
|
modelValue={settings.msDisplay}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
|
@ -36,47 +36,87 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import type { Ref, WatchStopHandle } from "vue";
|
import type { Ref, WatchStopHandle } from "vue";
|
||||||
import { computed, unref, watch } from "vue";
|
import { computed, unref, watch } from "vue";
|
||||||
|
|
||||||
export const ChallengeType = Symbol("ChallengeType");
|
/** A symbol used to identify {@link Challenge} features. */
|
||||||
|
export const ChallengeType = Symbol("Challenge");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Challenge}.
|
||||||
|
*/
|
||||||
export interface ChallengeOptions {
|
export interface ChallengeOptions {
|
||||||
|
/** Whether this challenge should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** Whether this challenge can be started. */
|
||||||
canStart?: Computable<boolean>;
|
canStart?: Computable<boolean>;
|
||||||
|
/** The reset function for this challenge. */
|
||||||
reset?: GenericReset;
|
reset?: GenericReset;
|
||||||
|
/** The requirement(s) to complete this challenge. */
|
||||||
requirements: Requirements;
|
requirements: Requirements;
|
||||||
|
/** Whether or not completing this challenge should grant multiple completions if requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
|
||||||
maximize?: Computable<boolean>;
|
maximize?: Computable<boolean>;
|
||||||
|
/** The maximum number of times the challenge can be completed. */
|
||||||
completionLimit?: Computable<DecimalSource>;
|
completionLimit?: Computable<DecimalSource>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** The display to use for this challenge. */
|
||||||
display?: Computable<
|
display?: Computable<
|
||||||
| CoercableComponent
|
| CoercableComponent
|
||||||
| {
|
| {
|
||||||
|
/** A header to appear at the top of the display. */
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
|
/** The main text that appears in the display. */
|
||||||
description: CoercableComponent;
|
description: CoercableComponent;
|
||||||
|
/** A description of the current goal for this challenge. */
|
||||||
goal?: CoercableComponent;
|
goal?: CoercableComponent;
|
||||||
|
/** A description of what will change upon completing this challenge. */
|
||||||
reward?: CoercableComponent;
|
reward?: CoercableComponent;
|
||||||
|
/** A description of the current effect of this challenge. */
|
||||||
effectDisplay?: CoercableComponent;
|
effectDisplay?: CoercableComponent;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
/** A function that is called when the challenge is completed. */
|
||||||
onComplete?: VoidFunction;
|
onComplete?: VoidFunction;
|
||||||
|
/** A function that is called when the challenge is exited. */
|
||||||
onExit?: VoidFunction;
|
onExit?: VoidFunction;
|
||||||
|
/** A function that is called when the challenge is entered. */
|
||||||
onEnter?: VoidFunction;
|
onEnter?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
|
||||||
|
*/
|
||||||
export interface BaseChallenge {
|
export interface BaseChallenge {
|
||||||
|
/** An auto-generated ID for identifying challenges that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** The current amount of times this challenge can be completed. */
|
||||||
canComplete: Ref<DecimalSource>;
|
canComplete: Ref<DecimalSource>;
|
||||||
|
/** The current number of times this challenge has been completed. */
|
||||||
completions: Persistent<DecimalSource>;
|
completions: Persistent<DecimalSource>;
|
||||||
|
/** Whether or not this challenge has been completed. */
|
||||||
completed: Ref<boolean>;
|
completed: Ref<boolean>;
|
||||||
|
/** Whether or not this challenge's completion count is at its limit. */
|
||||||
maxed: Ref<boolean>;
|
maxed: Ref<boolean>;
|
||||||
|
/** Whether or not this challenge is currently active. */
|
||||||
active: Persistent<boolean>;
|
active: Persistent<boolean>;
|
||||||
|
/** A function to enter or leave the challenge. */
|
||||||
toggle: VoidFunction;
|
toggle: VoidFunction;
|
||||||
|
/**
|
||||||
|
* A function to complete this challenge.
|
||||||
|
* @param remainInChallenge - Optional parameter to specify if the challenge should remain active after completion.
|
||||||
|
*/
|
||||||
complete: (remainInChallenge?: boolean) => void;
|
complete: (remainInChallenge?: boolean) => void;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof ChallengeType;
|
type: typeof ChallengeType;
|
||||||
|
/** 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 that can be entered and exited, and have one or more completions with scaling requirements. */
|
||||||
export type Challenge<T extends ChallengeOptions> = Replace<
|
export type Challenge<T extends ChallengeOptions> = Replace<
|
||||||
T & BaseChallenge,
|
T & BaseChallenge,
|
||||||
{
|
{
|
||||||
|
@ -92,6 +132,7 @@ export type Challenge<T extends ChallengeOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Challenge} object. */
|
||||||
export type GenericChallenge = Replace<
|
export type GenericChallenge = Replace<
|
||||||
Challenge<ChallengeOptions>,
|
Challenge<ChallengeOptions>,
|
||||||
{
|
{
|
||||||
|
@ -102,6 +143,10 @@ export type GenericChallenge = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a challenge with the given options.
|
||||||
|
* @param optionsFunc Challenge options.
|
||||||
|
*/
|
||||||
export function createChallenge<T extends ChallengeOptions>(
|
export function createChallenge<T extends ChallengeOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
|
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
|
||||||
): Challenge<T> {
|
): Challenge<T> {
|
||||||
|
@ -248,6 +293,12 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will automatically complete a challenge when it's requirements are met.
|
||||||
|
* @param challenge The challenge to auto-complete
|
||||||
|
* @param autoActive Whether or not auto-completing should currently occur
|
||||||
|
* @param exitOnComplete Whether or not to exit the challenge after auto-completion
|
||||||
|
*/
|
||||||
export function setupAutoComplete(
|
export function setupAutoComplete(
|
||||||
challenge: GenericChallenge,
|
challenge: GenericChallenge,
|
||||||
autoActive: Computable<boolean> = true,
|
autoActive: Computable<boolean> = true,
|
||||||
|
@ -264,19 +315,27 @@ export function setupAutoComplete(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for taking an array of challenges where only one may be active at a time, and giving a ref to the one currently active (or null if none are active)
|
||||||
|
* @param challenges The list of challenges that are mutually exclusive
|
||||||
|
*/
|
||||||
export function createActiveChallenge(
|
export function createActiveChallenge(
|
||||||
challenges: GenericChallenge[]
|
challenges: GenericChallenge[]
|
||||||
): Ref<GenericChallenge | undefined> {
|
): Ref<GenericChallenge | null> {
|
||||||
return computed(() => challenges.find(challenge => challenge.active.value));
|
return computed(() => challenges.find(challenge => challenge.active.value) ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for reporting if any challenge in a list is currently active. Intended for preventing entering a challenge if another is already active.
|
||||||
|
* @param challenges List of challenges that are mutually exclusive
|
||||||
|
*/
|
||||||
export function isAnyChallengeActive(
|
export function isAnyChallengeActive(
|
||||||
challenges: GenericChallenge[] | Ref<GenericChallenge | undefined>
|
challenges: GenericChallenge[] | Ref<GenericChallenge | null>
|
||||||
): Ref<boolean> {
|
): Ref<boolean> {
|
||||||
if (isArray(challenges)) {
|
if (isArray(challenges)) {
|
||||||
challenges = createActiveChallenge(challenges);
|
challenges = createActiveChallenge(challenges);
|
||||||
}
|
}
|
||||||
return computed(() => (challenges as Ref<GenericChallenge | undefined>).value != null);
|
return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "game/settings" {
|
declare module "game/settings" {
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="isVisible(visibility)"
|
|
||||||
:style="[
|
|
||||||
{
|
|
||||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
|
||||||
},
|
|
||||||
unref(style) ?? {}
|
|
||||||
]"
|
|
||||||
:class="{ feature: true, milestone: true, done: unref(earned), ...unref(classes) }"
|
|
||||||
>
|
|
||||||
<component :is="unref(comp)" />
|
|
||||||
<Node :id="id" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="tsx">
|
|
||||||
import "components/common/features.css";
|
|
||||||
import Node from "components/Node.vue";
|
|
||||||
import type { StyleValue } from "features/feature";
|
|
||||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
|
||||||
import type { GenericMilestone } from "features/milestones/milestone";
|
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
|
||||||
import type { Component, UnwrapRef } from "vue";
|
|
||||||
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
visibility: {
|
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
display: {
|
|
||||||
type: processedPropType<UnwrapRef<GenericMilestone["display"]>>(
|
|
||||||
String,
|
|
||||||
Object,
|
|
||||||
Function
|
|
||||||
),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
earned: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { display } = toRefs(props);
|
|
||||||
|
|
||||||
const comp = shallowRef<Component | string>("");
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const currDisplay = unwrapRef(display);
|
|
||||||
if (currDisplay == null) {
|
|
||||||
comp.value = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isCoercableComponent(currDisplay)) {
|
|
||||||
comp.value = coerceComponent(currDisplay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Requirement = coerceComponent(currDisplay.requirement, "h3");
|
|
||||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
|
|
||||||
const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
|
|
||||||
comp.value = coerceComponent(
|
|
||||||
jsx(() => (
|
|
||||||
<span>
|
|
||||||
<Requirement />
|
|
||||||
{currDisplay.effectDisplay != null ? (
|
|
||||||
<div>
|
|
||||||
<EffectDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{currDisplay.optionsDisplay != null ? (
|
|
||||||
<div class="equal-spaced">
|
|
||||||
<OptionsDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
comp,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.milestone {
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
min-width: 120px;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
background-color: var(--locked);
|
|
||||||
border-width: 4px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.milestone.done {
|
|
||||||
background-color: var(--bought);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milestone :deep(.equal-spaced) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milestone :deep(.equal-spaced > *) {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,235 +0,0 @@
|
||||||
import Select from "components/fields/Select.vue";
|
|
||||||
import type {
|
|
||||||
CoercableComponent,
|
|
||||||
GenericComponent,
|
|
||||||
OptionsFunc,
|
|
||||||
Replace,
|
|
||||||
StyleValue
|
|
||||||
} from "features/feature";
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
GatherProps,
|
|
||||||
getUniqueID,
|
|
||||||
isVisible,
|
|
||||||
jsx,
|
|
||||||
setDefault,
|
|
||||||
Visibility
|
|
||||||
} from "features/feature";
|
|
||||||
import MilestoneComponent from "features/milestones/Milestone.vue";
|
|
||||||
import { globalBus } from "game/events";
|
|
||||||
import "game/notifications";
|
|
||||||
import type { Persistent } from "game/persistence";
|
|
||||||
import { persistent } from "game/persistence";
|
|
||||||
import player from "game/player";
|
|
||||||
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 { createLazyProxy } from "util/proxies";
|
|
||||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
|
||||||
import { computed, unref, watchEffect } from "vue";
|
|
||||||
import { useToast } from "vue-toastification";
|
|
||||||
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
export const MilestoneType = Symbol("Milestone");
|
|
||||||
|
|
||||||
export enum MilestoneDisplay {
|
|
||||||
All = "all",
|
|
||||||
//Last = "last",
|
|
||||||
Configurable = "configurable",
|
|
||||||
Incomplete = "incomplete",
|
|
||||||
None = "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MilestoneOptions {
|
|
||||||
visibility?: Computable<Visibility | boolean>;
|
|
||||||
shouldEarn?: () => boolean;
|
|
||||||
style?: Computable<StyleValue>;
|
|
||||||
classes?: Computable<Record<string, boolean>>;
|
|
||||||
display?: Computable<
|
|
||||||
| CoercableComponent
|
|
||||||
| {
|
|
||||||
requirement: CoercableComponent;
|
|
||||||
effectDisplay?: CoercableComponent;
|
|
||||||
optionsDisplay?: CoercableComponent;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
showPopups?: Computable<boolean>;
|
|
||||||
onComplete?: VoidFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseMilestone {
|
|
||||||
id: string;
|
|
||||||
earned: Persistent<boolean>;
|
|
||||||
complete: VoidFunction;
|
|
||||||
type: typeof MilestoneType;
|
|
||||||
[Component]: GenericComponent;
|
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Milestone<T extends MilestoneOptions> = Replace<
|
|
||||||
T & BaseMilestone,
|
|
||||||
{
|
|
||||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
|
||||||
style: GetComputableType<T["style"]>;
|
|
||||||
classes: GetComputableType<T["classes"]>;
|
|
||||||
display: GetComputableType<T["display"]>;
|
|
||||||
showPopups: GetComputableType<T["showPopups"]>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type GenericMilestone = Replace<
|
|
||||||
Milestone<MilestoneOptions>,
|
|
||||||
{
|
|
||||||
visibility: ProcessedComputable<Visibility | boolean>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export function createMilestone<T extends MilestoneOptions>(
|
|
||||||
optionsFunc?: OptionsFunc<T, BaseMilestone, GenericMilestone>
|
|
||||||
): Milestone<T> {
|
|
||||||
const earned = persistent<boolean>(false, false);
|
|
||||||
return createLazyProxy(() => {
|
|
||||||
const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
|
||||||
milestone.id = getUniqueID("milestone-");
|
|
||||||
milestone.type = MilestoneType;
|
|
||||||
milestone[Component] = MilestoneComponent as GenericComponent;
|
|
||||||
|
|
||||||
milestone.earned = earned;
|
|
||||||
milestone.complete = function () {
|
|
||||||
const genericMilestone = milestone as GenericMilestone;
|
|
||||||
earned.value = true;
|
|
||||||
genericMilestone.onComplete?.();
|
|
||||||
if (genericMilestone.display != null && unref(genericMilestone.showPopups) === true) {
|
|
||||||
const display = unref(genericMilestone.display);
|
|
||||||
const Display = coerceComponent(
|
|
||||||
isCoercableComponent(display) ? display : display.requirement
|
|
||||||
);
|
|
||||||
toast(
|
|
||||||
<>
|
|
||||||
<h3>Milestone earned!</h3>
|
|
||||||
<div>
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<Display />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
processComputable(milestone as T, "visibility");
|
|
||||||
setDefault(milestone, "visibility", Visibility.Visible);
|
|
||||||
const visibility = milestone.visibility as ProcessedComputable<Visibility | boolean>;
|
|
||||||
milestone.visibility = computed(() => {
|
|
||||||
const display = unref((milestone as GenericMilestone).display);
|
|
||||||
switch (settings.msDisplay) {
|
|
||||||
default:
|
|
||||||
case MilestoneDisplay.All:
|
|
||||||
return unref(visibility);
|
|
||||||
case MilestoneDisplay.Configurable:
|
|
||||||
if (
|
|
||||||
unref(milestone.earned) &&
|
|
||||||
!(
|
|
||||||
display != null &&
|
|
||||||
typeof display == "object" &&
|
|
||||||
"optionsDisplay" in (display as Record<string, unknown>)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return Visibility.None;
|
|
||||||
}
|
|
||||||
return unref(visibility);
|
|
||||||
case MilestoneDisplay.Incomplete:
|
|
||||||
if (unref(milestone.earned)) {
|
|
||||||
return Visibility.None;
|
|
||||||
}
|
|
||||||
return unref(visibility);
|
|
||||||
case MilestoneDisplay.None:
|
|
||||||
return Visibility.None;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
processComputable(milestone as T, "style");
|
|
||||||
processComputable(milestone as T, "classes");
|
|
||||||
processComputable(milestone as T, "display");
|
|
||||||
processComputable(milestone as T, "showPopups");
|
|
||||||
|
|
||||||
milestone[GatherProps] = function (this: GenericMilestone) {
|
|
||||||
const { visibility, display, style, classes, earned, id } = this;
|
|
||||||
return { visibility, display, style: unref(style), classes, earned, id };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (milestone.shouldEarn) {
|
|
||||||
const genericMilestone = milestone as GenericMilestone;
|
|
||||||
watchEffect(() => {
|
|
||||||
if (settings.active !== player.id) return;
|
|
||||||
if (
|
|
||||||
!genericMilestone.earned.value &&
|
|
||||||
isVisible(genericMilestone.visibility) &&
|
|
||||||
genericMilestone.shouldEarn?.()
|
|
||||||
) {
|
|
||||||
genericMilestone.earned.value = true;
|
|
||||||
genericMilestone.onComplete?.();
|
|
||||||
if (
|
|
||||||
genericMilestone.display != null &&
|
|
||||||
unref(genericMilestone.showPopups) === true
|
|
||||||
) {
|
|
||||||
const display = unref(genericMilestone.display);
|
|
||||||
const Display = coerceComponent(
|
|
||||||
isCoercableComponent(display) ? display : display.requirement
|
|
||||||
);
|
|
||||||
toast(
|
|
||||||
<>
|
|
||||||
<h3>Milestone earned!</h3>
|
|
||||||
<div>
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<Display />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return milestone as unknown as Milestone<T>;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "game/settings" {
|
|
||||||
interface Settings {
|
|
||||||
msDisplay: MilestoneDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalBus.on("loadSettings", settings => {
|
|
||||||
setDefault(settings, "msDisplay", MilestoneDisplay.All);
|
|
||||||
});
|
|
||||||
|
|
||||||
const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
|
|
||||||
label: camelToTitle(option),
|
|
||||||
value: option
|
|
||||||
}));
|
|
||||||
|
|
||||||
registerSettingField(
|
|
||||||
jsx(() => (
|
|
||||||
<Select
|
|
||||||
title={jsx(() => (
|
|
||||||
<span class="option-title">
|
|
||||||
Show milestones
|
|
||||||
<desc>Select which milestones to display based on criterias.</desc>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
options={msDisplayOptions}
|
|
||||||
onUpdate:modelValue={value => (settings.msDisplay = value as MilestoneDisplay)}
|
|
||||||
modelValue={settings.msDisplay}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
);
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import { NonPersistent, Persistent, State } from "game/persistence";
|
import type { Persistent, State } from "game/persistence";
|
||||||
import { persistent } from "game/persistence";
|
import { NonPersistent, persistent } from "game/persistence";
|
||||||
import player from "game/player";
|
|
||||||
import settings from "game/settings";
|
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal, { format, formatWhole } from "util/bignum";
|
import Decimal, { format, formatWhole } from "util/bignum";
|
||||||
import type { ProcessedComputable } from "util/computed";
|
import type { ProcessedComputable } from "util/computed";
|
||||||
|
|
Loading…
Reference in a new issue