Source: QuestManager.js

/**
 * Config file for creating quests.
 * @typedef {object} QuestManager.QuestConfig
 * @property {string} questID - ID of quest
 * @property {string} questName - Name of quest to put on the input option
 * @property {boolean} [questImportant=false] - If the quest should show a pink or grey heart on the quest log
 * @property {QuestManager.SubquestConfig[]} [subquests=[]] - Array of subquest configs if you don't want to call addSubquest manually
 * @example
 * {
 *     questID: "myQuest",
 *     questName: "This is my quest",
 *     questImportant: false,
 *     subquests: []
 * }
 */

/**
 * @typedef {object} QuestManager.Quest
 * @property {string} modID - ID of mod that was used to create this quest.
 * @property {string} questID - ID of quest
 * @property {string} questName - Name of quest
 * @property {boolean} questImportant - If the quest shows a pink or grey heart on the quest log
 * @property {object<QuestManager.Subquest>} subquests - List of all subquests under this quest
 * @example
 * {
 *     modID: "QB",
 *     questID: "myQuest",
 *     questName: "This is my quest",
 *     questImportant: false,
 *     subquests: {}
 * }
 */

/**
 * Config file for creating subquests
 * @typedef {object} QuestManager.SubquestConfig
 * @property {string} questID - ID of quest
 * @property {string} subquestID - ID of subquest
 * @property {function|boolean} questCondition - Only display quest if this condition is true
 * @property {function|boolean} questProgress - Condition for the quest to be complete
 * @property {string} [mapKey] - mapKey used in userInput
 * @property {boolean} [mapIcon=true] - Display quest icon on map
 * @property {string} [dialogueKey] - dialogueKey used in userInput
 * @property {function} [rewards] - Function called after completing quest
 * @property {boolean} [questStart] - If the quest should have a "!" icon
 * @property {obj} [questLog] - If the quest should appear on the quest log
 * @property {string} questLog.questDescription - Description of the quest
 * @property {function|string} [questLog.progress] - User's progress of the quest. If a function, it should return a string
 * @example
 * {
 *     questID: "myQuest",
 *     subquestID: "giveHandjob",
 *     questCondition: function () {
 *         // Quest is available only if this condition is met
 *         return GAME.quest.isComplete('townFuckQuest', 'Completed');
 *     },
 *     questProgress: function () {
 *         // Quest is completable only if this condition is met
 *         return GAME.girl.getGirlLevel('Queen', 'Hands') >= 5;
 *     },
 *     mapKey: "TownMayorOffice",
 *     dialogueKey: "myQuestgiveHandjob",
 *     rewards: function () {
 *          // Called when the player completes the quest
 *          GAME.girl.gainExp('Queen', 'Hands', 500);
 *     },
 *     questLog: {
 *          questDescription: "Queen must give handjobs to everyone in Easthollow!",
 *          progress: function () {
 *              return "Level 5 hands needed. Queen's level: " + GAME.girl.getGirlLevel('Queen', 'Hands');
 *          }
 *     }
 * }
 */

/**
 * @typedef {object} QuestManager.Subquest
 * @property {string} modID - ID of mod that was used to create this quest.
 * @property {string} questID - ID of quest
 * @property {string} subquestID - ID of subquest
 * @property {function|boolean} questCondition - Only display quest if this condition is true
 * @property {function|boolean} questProgress - Condition for the quest to be complete
 * @property {string} mapKey - mapKey used in userInput
 * @property {string} dialogueKey - dialogueKey used in userInput
 * @property {function} [rewards] - Function called after completing quest
 * @property {boolean} [questStart] - If the quest should have a "!" icon
 * @property {obj} [questLog] - If the quest should appear on the quest log
 * @property {string} questLog.questDescription - Description of the quest
 * @property {function|string} [questLog.progress] - User's progress of the quest. If a function, it should return a string
 */

/**
 * Quests will be stored under gameData.quests[id]
 * All mods have their own QuestManager to prevent questIDs from overlapping. If your mod needs to do things with other mod's quests you should make sure you use that mod's QuestManager.
 * The default QuestManager id is "QB".
 * @class QuestManager
 * @param {string} id
 */
class QuestManager {
    constructor(id) {
        this.id = id;
        this._quests = {};
    }

    /**
     * @method addQuest
     * @memberOf QuestManager
     * @instance
     * @param {QuestManager.QuestConfig} config
     */
    addQuest(config) {
        if (gameData.quests.hasOwnProperty(this.id) === false) {
            gameData.quests[this.id] = {};
        }

        config.subquests = config.subquests || [];
        config.questImportant = config.questImportant || false;

        if (gameData.quests[this.id].hasOwnProperty(config.questID) === false) {
            gameData.quests[this.id][config.questID] = {};
        }

        this._quests[config.questID] = {
            modID: this.id,
            questID: config.questID,
            questName: config.questName,
            questImportant: config.questImportant,
            subquests: {}
        };

        for (let i in config.subquests) {
            this.addSubquest(config.subquests[i]);
        }
    }

    /**
     * @method addSubquest
     * @memberOf QuestManager
     * @instance
     * @param {QuestManager.SubquestConfig} config
     */
    addSubquest(config) {
        config.questStart = config.questStart || false;
        config.mapKey = config.mapKey || "";
        config.dialogueKey = config.dialogueKey || "";
        if (config.mapIcon === undefined) {
            config.mapIcon = true;
        }

        if (gameData.quests[this.id][config.questID].hasOwnProperty(config.subquestID) === false) {
            gameData.quests[this.id][config.questID][config.subquestID] = false;
        }

        this._quests[config.questID].subquests[config.subquestID] = config;
        this._quests[config.questID].subquests[config.subquestID].modID = this.id;
    }

    /**
     * @method getQuestStatus
     * @memberOf QuestManager
     * @instance
     * @param {string} questID
     * @param {string} subquestID
     * @returns {boolean}
     */
    getQuestStatus(questID, subquestID) {
        return gameData.quests[this.id][questID][subquestID];
    }

    /**
     * @method setQuestStatus
     * @memberOf QuestManager
     * @instance
     * @param {string} questID
     * @param {string} subquestID
     * @param {*} variable
     */
    setQuestStatus(questID, subquestID, variable) {
        gameData.quests[this.id][questID][subquestID] = variable;
    }

    /**
     * Completing a quest sets the quest status to true and runs the rewards function if there is one.
     * @method complete
     * @memberOf QuestManager
     * @instance
     * @param {string} questID
     * @param {string} subquestID
     * @param {*} [rewardsParameter] - If you need to pass anything into the rewards function
     * @returns {Promise}
     */
    complete(questID, subquestID, rewardsParameter) {
        return new Promise((resolve) => {
            let currentNumQuests = GAME.numQuests();

            this.setQuestStatus(questID, subquestID, true);

            if (this.getQuest(questID, subquestID).hasOwnProperty('rewards')) {
                this.getQuest(questID, subquestID).rewards(rewardsParameter);
            }

            let newNumQuests = GAME.numQuests();

            if (currentNumQuests === newNumQuests && newNumQuests !== 0) {
                GAME.notify('Quest log Updated!');
            } else if (currentNumQuests < newNumQuests) {
                GAME.notify("New Quest!");
            } else if (currentNumQuests > newNumQuests) {
                GAME.notify('Quest Complete!');
            }

            resolve();
        })
    }

    /**
     * @method isComplete
     * @memberOf QuestManager
     * @instance
     * @param {string} questID
     * @param {string} subquestID
     * @returns {boolean}
     */
    isComplete(questID, subquestID) {
        return gameData.quests[this.id][questID][subquestID] === true;
    }

    /**
     * Returns subquests
     * @method getActiveQuests
     * @instance
     * @memberOf QuestManager
     * @returns {Subquest[]}
     */
    getActiveQuests() {
        let questArray = [];

        for (let questID in this._quests) {
            for (let subquestID in this._quests[questID].subquests) {
                let subquest = this._quests[questID].subquests[subquestID];

                if (typeof subquest.questCondition === "function") {
                    if (subquest.questCondition() === true && this.getQuestStatus(subquest.questID, subquest.subquestID) === false) {
                        questArray.push(subquest);
                    }
                } else if (subquest.questCondition === true && this.getQuestStatus(subquest.questID, subquest.subquestID) === false) {
                    questArray.push(subquest);
                }
            }
        }

        return questArray;
    }

    /**
     * @method getAllQuests
     * @memberOf QuestManager
     * @instance
     * @returns {object}
     */
    getAllQuests() {
        return this._quests;
    }

    /**
     * @method getQuest
     * @memberOf QuestManager
     * @instance
     * @param {string} questID
     * @param {string} [subquestID] - If you want the subquest put a subquestID
     * @returns {QuestManager.Quest|QuestManager.Subquest}
     */
    getQuest(questID, subquestID) {
        if (subquestID) {
            return this._quests[questID].subquests[subquestID];
        } else {
            return this._quests[questID];
        }
    }

    /**
     * @method getSubquestProgress
     * @memberOf QuestManager
     * @instance
     * @param {QuestManager.Subquest} subquest
     * @returns {*}
     */
    getSubquestProgress(subquest) {
        if (typeof subquest.questProgress === "function") {
            return subquest.questProgress();
        } else {
            return subquest.questProgress;
        }
    }
}