Questify: Make ignored quests less likely to be overwritten when switching accounts. (#332)

* Switch to IDs and remove public facing setting.

* Allow claimed to be ignored to prevent account switching from overwriting it.

* Fix unclaimed counting & context option.

* Fix array undefined check.

* Fix mark all ignored.

* Technically allow ignoring claimed quests.

* Technically allow ignoring expired quests.

* Prepare a setting for picking shared or private ignores.

* Implement ignore profile.

* Restructure settings.
This commit is contained in:
Etorix 2025-08-08 12:00:27 -07:00 • committed by GitHub
parent 6f8e93d128
commit e3a7d7fbbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 170 additions and 142 deletions

View file

@ -14,31 +14,39 @@ import definePlugin, { StartAt } from "@utils/types";
import { ContextMenuApi, Menu, NavigationRouter } from "@webpack/common";
import { JSX } from "react";
import { addIgnoredQuest, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings";
import { GuildlessServerListItem, Quest, QuestIcon, QuestMap, QuestStatus, RGB } from "./utils/components";
import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getQuestStatus, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, waitUntilEnrolled } from "./utils/misc";
import { addIgnoredQuest, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, questIsIgnored, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings";
import { ExcludedQuestMap, GuildlessServerListItem, Quest, QuestIcon, QuestMap, QuestStatus, RGB } from "./utils/components";
import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, waitUntilEnrolled } from "./utils/misc";
const patchedMobileQuests = new Set<string>();
export const activeQuestIntervals = new Map<string, { progressTimeout: NodeJS.Timeout; rerenderTimeout: NodeJS.Timeout; progress: number; duration: number, type: string; }>();
function questMenuUnignoreClicked(): void {
validateAndOverwriteIgnoredQuests("");
function questMenuUnignoreAllClicked(): void {
validateAndOverwriteIgnoredQuests([]);
}
function questMenuIgnoreClicked(): void {
function questMenuIgnoreAllClicked(): void {
const quests = (QuestsStore.quests as QuestMap);
const ignoredQuestsSet = new Set();
const excludedQuests = (QuestsStore.excludedQuests as ExcludedQuestMap);
const ignoredQuestsSet = new Set<string>();
const ignoredQuestIDs = getIgnoredQuestIDs();
for (const quest of quests.values()) {
const questName = normalizeQuestName(quest.config.messages.questName);
const questID = quest.id;
const questStatus = getQuestStatus(quest, false);
if (questStatus === QuestStatus.Unclaimed) {
ignoredQuestsSet.add(questName);
if (questStatus === QuestStatus.Unclaimed || ignoredQuestIDs.includes(questID)) {
ignoredQuestsSet.add(questID);
}
}
settings.store.ignoredQuests = Array.from(ignoredQuestsSet).join("\n");
for (const quest of excludedQuests.values()) {
if (ignoredQuestIDs.includes(quest.id)) {
ignoredQuestsSet.add(quest.id);
}
}
setIgnoredQuestIDs(Array.from(ignoredQuestsSet));
settings.store.unclaimedUnignoredQuests = 0;
}
@ -86,14 +94,14 @@ export function QuestButton(): JSX.Element {
<Menu.MenuItem
id={q("ignore-quests-option")}
label="Mark All Ignored"
action={questMenuIgnoreClicked}
action={questMenuIgnoreAllClicked}
disabled={!unclaimedUnignoredQuests}
/>
<Menu.MenuItem
id={q("unignore-quests-option")}
label="Reset Ignored List"
action={questMenuUnignoreClicked}
disabled={!settings.store.ignoredQuests}
action={questMenuUnignoreAllClicked}
disabled={!getIgnoredQuestIDs().length}
/>
<Menu.MenuItem
id={q("fetch-quests-option")}
@ -198,10 +206,10 @@ function shouldHideFriendsListActiveNowPromotion(): boolean {
}
function shouldDisableQuestTileOptions(quest: Quest, shouldBeIgnored: boolean): boolean {
const questStatus = getQuestStatus(quest);
const isIgnored = questIsIgnored(quest.id);
return !(
(shouldBeIgnored ? questStatus === QuestStatus.Ignored : questStatus === QuestStatus.Unclaimed)
(shouldBeIgnored ? isIgnored : !isIgnored)
);
}
@ -212,13 +220,13 @@ function QuestTileContextMenu(children: React.ReactNode[], props: { quest: any;
id={q("ignore-quests")}
label="Mark as Ignored"
disabled={shouldDisableQuestTileOptions(props.quest, false)}
action={() => { addIgnoredQuest(props.quest.config.messages.questName); }}
action={() => { addIgnoredQuest(props.quest.id); }}
/>
<Menu.MenuItem
id={q("unignore-quests")}
label="Unmark as Ignored"
disabled={shouldDisableQuestTileOptions(props.quest, true)}
action={() => { removeIgnoredQuest(props.quest.config.messages.questName); }}
action={() => { removeIgnoredQuest(props.quest.id); }}
/>
{activeQuestIntervals.has(props.quest.id) &&
<Menu.MenuItem
@ -242,19 +250,21 @@ function QuestTileContextMenu(children: React.ReactNode[], props: { quest: any;
export function getQuestTileClasses(originalClasses: string, quest: Quest, color: number | null | undefined, gradient: string | undefined): string {
const {
ignoredQuestIDs,
ignoredQuestProfile,
restyleQuestsUnclaimed,
restyleQuestsClaimed,
restyleQuestsIgnored,
restyleQuestsExpired,
restyleQuestsGradient,
ignoredQuests
restyleQuestsGradient
} = settings.use([
"ignoredQuestIDs",
"ignoredQuestProfile",
"restyleQuestsUnclaimed",
"restyleQuestsClaimed",
"restyleQuestsIgnored",
"restyleQuestsExpired",
"restyleQuestsGradient",
"ignoredQuests"
"restyleQuestsGradient"
]);
const customClasses = [
@ -284,12 +294,12 @@ export function getQuestTileClasses(originalClasses: string, quest: Quest, color
} else if (questStatus === QuestStatus.Unclaimed && (color || restyleQuestsUnclaimed !== null)) {
returnClasses.push(q("quest-item-restyle"));
isRestyledAndDarkish = isDarkish(decimalToRGB(color ?? restyleQuestsUnclaimed), 0.875);
} else if (questStatus === QuestStatus.Ignored && (color || restyleQuestsIgnored !== null)) {
returnClasses.push(q("quest-item-restyle"));
isRestyledAndDarkish = isDarkish(decimalToRGB(color ?? restyleQuestsIgnored), 0.875);
} else if (questStatus === QuestStatus.Expired && (color || restyleQuestsExpired !== null)) {
returnClasses.push(q("quest-item-restyle"));
isRestyledAndDarkish = isDarkish(decimalToRGB(color ?? restyleQuestsExpired), 0.875);
} else if (questStatus === QuestStatus.Ignored && (color || restyleQuestsIgnored !== null)) {
returnClasses.push(q("quest-item-restyle"));
isRestyledAndDarkish = isDarkish(decimalToRGB(color ?? restyleQuestsIgnored), 0.875);
}
}
@ -316,7 +326,8 @@ export function getQuestTileClasses(originalClasses: string, quest: Quest, color
function preprocessQuests(quests: Quest[]): Quest[] {
const {
ignoredQuests,
ignoredQuestIDs,
ignoredQuestProfile,
reorderQuests,
unclaimedSubsort,
claimedSubsort,
@ -327,7 +338,8 @@ function preprocessQuests(quests: Quest[]): Quest[] {
completeGameQuestsInBackground,
triggerQuestsRerender
} = settings.use([
"ignoredQuests",
"ignoredQuestIDs",
"ignoredQuestProfile",
"reorderQuests",
"unclaimedSubsort",
"claimedSubsort",
@ -387,10 +399,10 @@ function preprocessQuests(quests: Quest[]): Quest[] {
questGroups.claimed.push(quest);
} else if (questStatus === QuestStatus.Unclaimed) {
questGroups.unclaimed.push(quest);
} else if (questStatus === QuestStatus.Ignored) {
questGroups.ignored.push(quest);
} else if (questStatus === QuestStatus.Expired) {
questGroups.expired.push(quest);
} else if (questStatus === QuestStatus.Ignored) {
questGroups.ignored.push(quest);
} else {
questGroups.unknown.push(quest);
}
@ -453,10 +465,12 @@ function preprocessQuests(quests: Quest[]): Quest[] {
export function getQuestTileStyle(quest: Quest | null): Record<string, string> {
const {
restyleQuests,
ignoredQuests,
ignoredQuestIDs,
ignoredQuestProfile
} = settings.use([
"restyleQuests",
"ignoredQuests",
"ignoredQuestIDs",
"ignoredQuestProfile"
]);
const style: Record<string, string> = {};
@ -479,10 +493,10 @@ export function getQuestTileStyle(quest: Quest | null): Record<string, string> {
themeColor = dummyProvided ? dummyColor : claimedColor || null;
} else if (questStatus === QuestStatus.Unclaimed) {
themeColor = dummyProvided ? dummyColor : unclaimedColor || null;
} else if (questStatus === QuestStatus.Ignored) {
themeColor = dummyProvided ? dummyColor : ignoredColor || null;
} else if (questStatus === QuestStatus.Expired) {
themeColor = dummyProvided ? dummyColor : expiredColor || null;
} else if (questStatus === QuestStatus.Ignored) {
themeColor = dummyProvided ? dummyColor : ignoredColor || null;
}
if (!themeColor) return style;

View file

@ -9,12 +9,12 @@ import { ErrorBoundary } from "@components/index";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findLazy } from "@webpack";
import { Button, ColorPicker, ContextMenuApi, Forms, Menu, Select, TextArea, TextInput, useEffect, useRef, useState } from "@webpack/common";
import { Button, ColorPicker, ContextMenuApi, Forms, Menu, Select, TextInput, useEffect, useRef, useState } from "@webpack/common";
import { JSX } from "react";
import { activeQuestIntervals, getQuestTileClasses, getQuestTileStyle } from "./index";
import { DynamicDropdown, DynamicDropdownSettingOption, GuildlessServerListItem, Quest, QuestIcon, QuestStatus, QuestTile, RadioGroup, RadioOption, SelectOption, SoundIcon } from "./utils/components";
import { AudioPlayer, decimalToRGB, fetchAndDispatchQuests, getFormattedNow, getQuestStatus, isDarkish, isSoundAllowed, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, QuestsStore, rightClick, validCommaSeparatedList } from "./utils/misc";
import { DynamicDropdown, DynamicDropdownSettingOption, ExcludedQuest, GuildlessServerListItem, Quest, QuestIcon, QuestStatus, QuestTile, RadioGroup, RadioOption, SelectOption, SoundIcon } from "./utils/components";
import { AudioPlayer, decimalToRGB, fetchAndDispatchQuests, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, isSoundAllowed, leftClick, middleClick, q, QuestifyLogger, QuestsStore, rightClick, setIgnoredQuestIDs, validCommaSeparatedList } from "./utils/misc";
let defaultSounds: string[] | null = null;
let autoFetchInterval: null | ReturnType<typeof setInterval> = null;
@ -129,49 +129,54 @@ export const intervalScales = {
}
};
export function removeIgnoredQuest(questName: string): void {
const ignoredQuests = settings.store.ignoredQuests.split("\n");
validateAndOverwriteIgnoredQuests(ignoredQuests.filter(name => normalizeQuestName(name) !== normalizeQuestName(questName)).join("\n"));
export function removeIgnoredQuest(questID: string): void {
const ignoredQuests = getIgnoredQuestIDs();
const newIgnoredQuests = ignoredQuests.filter(id => id !== questID);
validateAndOverwriteIgnoredQuests(newIgnoredQuests);
}
export function addIgnoredQuest(questName: string): void {
const { ignoredQuests } = settings.store;
validateAndOverwriteIgnoredQuests(ignoredQuests + "\n" + normalizeQuestName(questName));
export function addIgnoredQuest(questID: string): void {
const ignoredQuests = getIgnoredQuestIDs();
const newIgnoredQuests = ignoredQuests.concat(questID);
validateAndOverwriteIgnoredQuests(newIgnoredQuests);
}
export function questIsIgnored(questName: string): boolean {
const ignoredQuests = new Set(settings.store.ignoredQuests.split("\n").map(normalizeQuestName));
return ignoredQuests.has(normalizeQuestName(questName));
export function questIsIgnored(questID: string): boolean {
const ignoredQuests = getIgnoredQuestIDs();
return ignoredQuests.includes(questID);
}
export function validateIgnoredQuests(ignoredQuests?: string, questsData?: Quest[]): [string, number] {
export function validateIgnoredQuests(ignoredQuests?: string[], questsData?: Quest[]): [string[], number] {
const quests = questsData ?? Array.from(QuestsStore.quests.values()) as Quest[];
const currentlyIgnored = new Set((ignoredQuests ?? settings.store.ignoredQuests).split("\n").map(normalizeQuestName));
const excludedQuests = Array.from(QuestsStore.excludedQuests.values()) as ExcludedQuest[];
const currentlyIgnored = ignoredQuests ? new Set(ignoredQuests) : new Set(getIgnoredQuestIDs());
const validIgnored = new Set<string>();
let numUnclaimedUnignoredQuests = 0;
for (const quest of quests) {
const questStatus = getQuestStatus(quest, false);
const normalizedName = normalizeQuestName(quest.config.messages.questName);
if (questStatus === QuestStatus.Unclaimed) {
if (currentlyIgnored.has(normalizedName)) {
validIgnored.add(normalizedName);
} else {
numUnclaimedUnignoredQuests++;
}
if (currentlyIgnored.has(quest.id)) {
validIgnored.add(quest.id);
} else if (questStatus === QuestStatus.Unclaimed) {
numUnclaimedUnignoredQuests++;
}
}
const ignoredStr = Array.from(validIgnored).join("\n");
return [ignoredStr, numUnclaimedUnignoredQuests];
for (const quest of excludedQuests) {
if (currentlyIgnored.has(quest.id)) {
validIgnored.add(quest.id);
}
}
return [Array.from(validIgnored), numUnclaimedUnignoredQuests];
}
export function validateAndOverwriteIgnoredQuests(ignoredQuests?: string, questsData?: Quest[]): string {
const [ignoredStr, numUnclaimedUnignoredQuests] = validateIgnoredQuests(ignoredQuests, questsData);
export function validateAndOverwriteIgnoredQuests(ignoredQuests?: string[], questsData?: Quest[]): string[] {
const [validIgnored, numUnclaimedUnignoredQuests] = validateIgnoredQuests(ignoredQuests, questsData);
settings.store.unclaimedUnignoredQuests = numUnclaimedUnignoredQuests;
settings.store.ignoredQuests = ignoredStr;
return ignoredStr;
setIgnoredQuestIDs(validIgnored);
return validIgnored;
}
interface DummyQuestButtonProps {
@ -689,48 +694,6 @@ function DisableQuestsSetting(): JSX.Element {
);
}
function IgnoredQuestsSetting(): JSX.Element {
const [ignoredQuests, setIgnoredQuests] = useState(settings.store.ignoredQuests);
validateAndOverwriteIgnoredQuests(ignoredQuests);
return (
<ErrorBoundary>
<Forms.FormDivider className={q("setting-divider")} />
<div className={q("setting", "ignored-quests-setting")}>
<Forms.FormSection>
<div>
<Forms.FormTitle className={q("form-title")}>
Ignored Quests
</Forms.FormTitle>
<Forms.FormText className={q("form-description")}>
A list of Quest names to exclude from the <span className={q("inline-code-block")}>Unclaimed Indicator</span>.
<br /><br />
One Quest name per line. Names must match the spelling displayed on the Quests page.
Alternatively, click the three dots on a Quest tile and select the <span className={q("inline-code-block")}>Mark as Ignored</span> option.
</Forms.FormText>
</div>
<div>
<TextArea
className={q("text-area")}
value={ignoredQuests}
onChange={newValue => {
setIgnoredQuests(newValue);
const [validated, badgeNum] = validateIgnoredQuests(newValue);
if (validated.trim() === newValue.trim()) {
settings.store.ignoredQuests = validated;
settings.store.unclaimedUnignoredQuests = badgeNum;
}
}}
/>
</div>
</Forms.FormSection>
</div>
<Forms.FormDivider className={q("setting-divider")} />
</ErrorBoundary>
);
}
const DummyQuestPreview = ({ quest, dummyColor, dummyGradient }: { quest: Quest; dummyColor: number | null; dummyGradient: string; }) => {
const classes = getQuestTileClasses("", quest, dummyColor, dummyGradient);
@ -749,14 +712,16 @@ function RestyleQuestsSetting() {
restyleQuestsIgnored,
restyleQuestsExpired,
restyleQuestsGradient,
restyleQuestsPreload
restyleQuestsPreload,
ignoredQuestProfile
} = settings.use([
"restyleQuestsUnclaimed",
"restyleQuestsClaimed",
"restyleQuestsIgnored",
"restyleQuestsExpired",
"restyleQuestsGradient",
"restyleQuestsPreload"
"restyleQuestsPreload",
"ignoredQuestProfile"
]);
const [unclaimedColor, setUnclaimedColor] = useState<number | null>(restyleQuestsUnclaimed);
@ -874,8 +839,58 @@ function RestyleQuestsSetting() {
</Forms.FormTitle>
<Forms.FormText className={q("form-description")}>
Highlight Quests with optional theme colors for visibility.
<br /><br />
Claimed and Expired Quest styles will take precedence even if a Quest is ignored.
</Forms.FormText>
</div>
<div className={q("main-inline-group")}>
<div className={q("gradient-setting-group", "inline-group-item", "flex-35")}>
<Forms.FormTitle className={q("form-subtitle")}>
Gradient Style
</Forms.FormTitle>
<Select
options={gradientOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === restyleQuestsGradientValue}
select={handleGradientChange}
/>
</div>
<div className={q("preload-setting-group", "inline-group-item", "flex-65")}>
<Forms.FormTitle className={q("form-subtitle")}>
Asset Preload
</Forms.FormTitle>
<Select
options={preloadOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: boolean) => value === restyleQuestsPreloadValue}
select={handlePreloadChange}
/>
</div>
</div>
<div className={q("main-inline-group")}>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle")}>
Ignored Quest Profile
</Forms.FormTitle>
<Select
options={[
{ label: "Shared: All accounts on this client share ignores.", value: "shared" },
{ label: "Private: All accounts on this client have separate ignores.", value: "private" }
]}
className={q("select")}
popoutPosition="bottom"
serialize={String}
isSelected={(value: string) => value === ignoredQuestProfile}
select={(value: string) => {
settings.store.ignoredQuestProfile = value;
}}
/>
</div>
</div>
<div className={q("color-picker-container")}>
{colorPickers.map(({ label, idx, defaultValue, value }) => (
<div
@ -909,34 +924,6 @@ function RestyleQuestsSetting() {
</div>
))}
</div>
<div className={q("main-inline-group")}>
<div className={q("gradient-setting-group", "inline-group-item", "flex-35")}>
<Forms.FormTitle className={q("form-subtitle")}>
Gradient Style
</Forms.FormTitle>
<Select
options={gradientOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === restyleQuestsGradientValue}
select={handleGradientChange}
/>
</div>
<div className={q("preload-setting-group", "inline-group-item", "flex-65")}>
<Forms.FormTitle className={q("form-subtitle")}>
Asset Preload
</Forms.FormTitle>
<Select
options={preloadOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: boolean) => value === restyleQuestsPreloadValue}
select={handlePreloadChange}
/>
</div>
</div>
<div className={q("dummy-quest-preview")} style={dummyQuestStyle}>
{hasQuests && dummyQuest && (
<DummyQuestPreview quest={dummyQuest} dummyColor={dummyColor} dummyGradient={dummyGradient} />
@ -1644,11 +1631,6 @@ export const settings = definePluginSettings({
default: "Expiring DESC", // "Recent ASC", "Recent DESC", "Expiring ASC", "Expiring DESC"
hidden: true
},
ignoredQuests: {
type: OptionType.COMPONENT,
default: "",
component: IgnoredQuestsSetting
},
unclaimedUnignoredQuests: {
type: OptionType.NUMBER,
description: "Tracks the number of unclaimed and unignored Quests.",
@ -1667,4 +1649,16 @@ export const settings = definePluginSettings({
default: false,
hidden: true
},
ignoredQuestProfile: {
type: OptionType.STRING,
description: "The profile used for ignored Quests.",
default: "private", // "shared", "private"
hidden: true
},
ignoredQuestIDs: {
type: OptionType.CUSTOM,
description: "An array of Quest IDs that are ignored.",
default: {} as Record<string, string[]>,
hidden: true,
}
});

View file

@ -55,8 +55,14 @@ export interface RGB {
b: number;
}
export type ExcludedQuestMap = Map<string, ExcludedQuest>;
export type QuestMap = Map<string, Quest>;
export interface ExcludedQuest {
id: string;
}
export interface Quest {
questifyNumber: number;
id: string;

View file

@ -7,9 +7,9 @@
import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, RestAPI } from "@webpack/common";
import { FluxDispatcher, RestAPI, UserStore } from "@webpack/common";
import { questIsIgnored } from "../settings";
import { questIsIgnored, settings } from "../settings";
import { Quest, QuestStatus, RGB } from "./components";
export const q = classNameFactory("questify-");
@ -25,16 +25,30 @@ export const leftClick = 0;
export const middleClick = 1;
export const rightClick = 2;
export function setIgnoredQuestIDs(questIDs: string[]): void {
const currentUserID = UserStore.getCurrentUser().id;
const { ignoredQuestProfile } = settings.store;
const key = ignoredQuestProfile === "shared" ? "shared" : currentUserID;
settings.store.ignoredQuestIDs[key] = questIDs;
}
export function getIgnoredQuestIDs(): string[] {
const currentUserID = UserStore.getCurrentUser().id;
const { ignoredQuestIDs, ignoredQuestProfile } = settings.store;
const key = ignoredQuestProfile === "shared" ? "shared" : currentUserID;
ignoredQuestIDs[key] ??= [];
return ignoredQuestIDs[key];
}
export function getQuestStatus(quest: Quest, checkIgnored: boolean = true): QuestStatus {
const questName = normalizeQuestName(quest.config.messages.questName);
const completedQuest = quest.userStatus?.completedAt;
const claimedQuest = quest.userStatus?.claimedAt;
const expiredQuest = new Date(quest.config.expiresAt) < new Date();
const questIgnored = questIsIgnored(questName);
const questIgnored = questIsIgnored(quest.id);
if (claimedQuest) {
return QuestStatus.Claimed;
} else if (checkIgnored && questIgnored) {
} else if (checkIgnored && questIgnored && (!expiredQuest || completedQuest)) {
return QuestStatus.Ignored;
} else if (completedQuest || !expiredQuest) {
return QuestStatus.Unclaimed;