Melvor Idle - AutoFarm

Automates farming

// ==UserScript==
// @name        Melvor Idle - AutoFarm
// @description Automates farming
// @version     1.5
// @namespace   Visua
// @match       https://*.melvoridle.com/*
// @exclude     https://wiki.melvoridle.com*
// @noframes
// @grant       none
// ==/UserScript==
/* jshint esversion: 6 */

((main) => {
    var script = document.createElement('script');
    script.textContent = `try { (${main})(); } catch (e) { console.log(e); }`;
    document.body.appendChild(script).parentNode.removeChild(script);
})(() => {
    'use strict';

    function startAutoFarm() {
        const utils = {
            equipFromBank: function (itemId, quantity = 1) {
                if (!checkBankForItem(itemId)) {
                    return false;
                }
                try {
                    return combatManager.player.equipItem(itemId, combatManager.player.selectedEquipmentSet, 'Default', quantity);
                } catch (e) {
                    console.error('AutoFarm: Error trying to equip item.', e);
                    return false;
                }
            },

            equipSwapState: {
                Ring: {},
                Cape: {},
                Gloves: {},
                Quiver: {},
            },

            equipSwap: function (slotName, itemId) {
                if (this.equipSwapState[slotName].swapped) {
                    return;
                }
                const currentlyEquippedItemId = combatManager.player.equipment.slots[slotName].item.id;
                const currentQuantity = combatManager.player.equipment.slots[slotName].quantity;
                let quantityToEquip = slotName === 'Quiver' ? getBankQty(itemId) : 1;
                if (this.equipFromBank(itemId, quantityToEquip)) {
                    this.equipSwapState[slotName].originalId = currentlyEquippedItemId;
                    this.equipSwapState[slotName].quantity = currentQuantity;
                    this.equipSwapState[slotName].swapped = true;
                }
            },

            equipSwapBack: function (slotName) {
                if (this.equipSwapState[slotName].swapped && (this.equipSwapState[slotName].originalId === -1 ||
                    this.equipFromBank(this.equipSwapState[slotName].originalId, this.equipSwapState[slotName].quantity))) {
                    this.equipSwapState[slotName].originalId = undefined;
                    this.equipSwapState[slotName].swapped = false;
                }
            },
        };

        const id = 'auto-farm';
        const settingsVersion = 2;
        const patchTypes = ['allotments', 'herbs', 'trees'];
        const toPatchType = { ALLOTMENT: patchTypes[0], HERB: patchTypes[1], TREE: patchTypes[2] };
        const priorityTypes = {
            custom: { id: 'custom', description: 'Custom priority', tooltip: 'Drag seeds to change their priority' },
            mastery: {
                id: 'mastery',
                description: 'Highest mastery',
                tooltip: 'Seeds with maxed mastery are excluded<br>Click seeds to disable/enable them',
            },
            replant: { id: 'replant', description: 'Replant', tooltip: 'Lock patches to their current seeds' },
            quantity: {
                id: 'quantity',
                description: 'Lowest quantity',
                tooltip: 'Crops with the lowest quantity in the bank are planted',
            },
        };
        const allSeeds = {
            allotments: [...allotmentSeeds].sort((a, b) => b.level - a.level).map((s) => s.itemID),
            herbs: [...herbSeeds].sort((a, b) => b.level - a.level).map((s) => s.itemID),
            trees: [...treeSeeds].sort((a, b) => b.level - a.level).map((s) => s.itemID),
        };
        const compostShopId = isItemInShop(CONSTANTS.item.Compost);
        let observer;
        let settings = {
            version: settingsVersion,
            disabledSeeds: {},
            swapEquipment: true,
        };
        patchTypes.forEach((patchType) => {
            settings[patchType] = {
                enabled: false,
                priorityType: priorityTypes.custom.id,
                priority: allSeeds[patchType],
                lockedPatches: {},
                useGloop: true,
            };
        });

        function findNextSeed(patch, patchId) {
            // Find next seed in bank according to priority
            const patchType = toPatchType[patch.type];
            const patchTypeSettings = settings[patchType];
            const lockedSeed = patchTypeSettings.lockedPatches[patchId];
            let priority = [];
            if (lockedSeed !== undefined) {
                priority = [lockedSeed];
            } else if (patchTypeSettings.priorityType === priorityTypes.custom.id) {
                priority = patchTypeSettings.priority;
            } else if (patchTypeSettings.priorityType === priorityTypes.mastery.id) {
                priority = allSeeds[patchType]
                    .filter((s) => !settings.disabledSeeds[s] && getSeedMasteryLevel(s) < 99)
                    .sort((a, b) => getSeedMastery(b) - getSeedMastery(a));
            } else if (patchTypeSettings.priorityType === priorityTypes.quantity.id) {
                priority = allSeeds[patchType]
                    .filter((s) => !settings.disabledSeeds[s])
                    .sort((a, b) => getSeedCropQuantity(a) - getSeedCropQuantity(b));
            }

            let nextSeed = -1;
            for (let k = 0; k < priority.length; k++) {
                const seedId = priority[k];
                if (seedId !== -1 && skillLevel[CONSTANTS.skill.Farming] >= items[seedId].farmingLevel) {
                    const bankId = getBankId(seedId);
                    if (bankId !== -1 && bank[bankId].qty >= items[seedId].seedsRequired) {
                        nextSeed = seedId;
                        break;
                    }
                }
            }

            return nextSeed;
        }

        function handlePatch(areaId, patchId) {
            const patch = newFarmingAreas[areaId].patches[patchId];

            if (!settings[toPatchType[patch.type]].enabled || !patch.unlocked || !(patch.hasGrown || !patch.seedID)) {
                // AutoFarm disabled for patch type or patch not unlocked or still growing
                return;
            }

            if (patch.hasGrown) {
                // Harvest
                let grownId = items[patch.seedID].grownItemID;
                let bankId = getBankId(grownId);
                if (bankId === -1 && bank.length >= getMaxBankSpace()) {
                    return;
                }
                harvestSeed(areaId, patchId);
            }

            const nextSeed = findNextSeed(patch, patchId);
            if (nextSeed === -1) {
                // No seeds available
                return;
            }

            if (!patch.gloop) {
                if (settings[toPatchType[patch.type]].useGloop && getBankQty(CONSTANTS.item.Weird_Gloop) > 1) {
                    addGloop(areaId, patchId);
                } else if (
                    getSeedMasteryLevel(nextSeed) < 50 &&
                    getMasteryPoolProgress(CONSTANTS.skill.Farming) < masteryCheckpoints[1]
                ) {
                    if (!playerModifiers.freeCompost) {
                        buyCompost();
                    }
                    addCompost(areaId, patchId, 5);
                }
            }
            selectedPatch = [areaId, patchId];
            selectedSeed = nextSeed;
            plantSeed();
        }

        function autoFarm() {
            let anyPatchReady = false;
            for (let i = 0; i < newFarmingAreas.length; i++) {
                for (let j = 0; j < newFarmingAreas[i].patches.length; j++) {
                    const patch = newFarmingAreas[i].patches[j];
                    if (
                        settings[toPatchType[patch.type]].enabled &&
                        patch.unlocked &&
                        (patch.hasGrown || (!patch.seedID && findNextSeed(patch, j) !== -1))
                    ) {
                        anyPatchReady = true;
                        break;
                    }
                }
            }
            if (anyPatchReady) {
                swapToFarmingEquipment();
                for (let i = 0; i < newFarmingAreas.length; i++) {
                    for (let j = 0; j < newFarmingAreas[i].patches.length; j++) {
                        handlePatch(i, j);
                    }
                }
                swapBackEquipment();
            }

            patchTypes.forEach((patchType) => {
                if (settings[patchType].priorityType === priorityTypes.mastery.id) {
                    orderMasteryPriorityMenu(patchType);
                } else if (settings[patchType].priorityType === priorityTypes.quantity.id) {
                    orderQuantityPriorityMenu(patchType);
                }
            });
        }

        function equipIfNotEquipped(slotName, itemId) {
            if (combatManager.player.equipment.slotMap.has(itemId)) {
                return true;
            }
            if (checkRequirements(items[itemId].equipRequirements) && checkBankForItem(itemId)) {
                utils.equipSwap(slotName, itemId);
                return true;
            }
            return false;
        }

        function swapToFarmingEquipment() {
            if (!settings.swapEquipment) {
                return;
            }

            equipIfNotEquipped('Ring', CONSTANTS.item.Aorpheats_Signet_Ring);
            equipIfNotEquipped('Cape', CONSTANTS.item.Cape_of_Completion) ||
                equipIfNotEquipped('Cape', CONSTANTS.item.Max_Skillcape) ||
                equipIfNotEquipped('Cape', CONSTANTS.item.Farming_Skillcape);
            equipIfNotEquipped('Gloves', CONSTANTS.item.Bobs_Gloves);
            equipIfNotEquipped('Quiver', CONSTANTS.item.Seed_Pouch);
        }

        function swapBackEquipment() {
            if (!settings.swapEquipment) {
                return;
            }

            Object.keys(utils.equipSwapState).forEach(slot => utils.equipSwapBack(slot));
        }

        function buyCompost() {
            const qty = getBankQty(CONSTANTS.item.Compost);
            if (qty < 5) {
                buyQty = 5 - qty;
                if (gp > buyQty * items[CONSTANTS.item.Compost].buysFor) {
                    buyShopItem(Object.keys(SHOP)[compostShopId[0]], compostShopId[1], true);
                }
            }
        }

        function getSeedMastery(seedId) {
            return MASTERY[CONSTANTS.skill.Farming].xp[items[seedId].masteryID[1]];
        }

        function getSeedCropQuantity(seedId) {
            return getBankQty(items[seedId].grownItemID);
        }

        function getSeedMasteryLevel(seedId) {
            return getMasteryLevel(CONSTANTS.skill.Farming, items[seedId].masteryID[1]);
        }

        function orderMasteryPriorityMenu(patchType) {
            const menu = $(`#${id}-${patchType}-prioritysettings-mastery`);
            menu.children()
                .toArray()
                .filter((e) => getSeedMasteryLevel($(e).data('seed-id')) >= 99)
                .forEach((e) => $(e).remove());
            const sortedMenuItems = menu
                .children()
                .toArray()
                .sort((a, b) => getSeedMastery($(b).data('seed-id')) - getSeedMastery($(a).data('seed-id')));
            menu.append(sortedMenuItems);
        }

        function orderQuantityPriorityMenu(patchType) {
            const menu = $(`#${id}-${patchType}-prioritysettings-quantity`);
            const sortedMenuItems = menu
                .children()
                .toArray()
                .sort((a, b) => getSeedCropQuantity($(a).data('seed-id')) - getSeedCropQuantity($(b).data('seed-id')));
            menu.append(sortedMenuItems);
        }

        function injectGUI() {
            if ($(`#${id}`).length) {
                return;
            }

            const disabledOpacity = 0.25;

            function saveSettings() {
                localStorage.setItem(`${id}-config-${currentCharacter}`, JSON.stringify(settings));
            }

            function loadSettings() {
                const storedSettings = JSON.parse(localStorage.getItem(`${id}-config-${currentCharacter}`));
                if (!storedSettings) {
                    return;
                }

                settings = { ...settings, ...storedSettings };

                // Update old settings
                if (settings.version === 1) {
                    patchTypes.forEach((patchType) => {
                        settings[patchType].useGloop = true;
                    });
                }
                settings.version = settingsVersion;
                saveSettings();
            }
            loadSettings();
            window.AUTOFARM_SETTINGS = settings;

            function createPatchTypeDiv(patchType) {
                function createSeedDiv(seedId) {
                    const grownItem = items[items[seedId].grownItemID];
                    return `
                    <div class="btn btn-outline-secondary ${id}-priority-selector" data-seed-id="${seedId}" data-tippy-content="${grownItem.name}" style="margin: 2px; padding: 6px; float: left;">
                        <img src="${grownItem.media}" width="30" height="30">
                    </div>`;
                }

                function createPriorityTypeSelector(priorityType) {
                    const prefix = `${id}-${patchType}-prioritytype`;
                    const elementId = `${prefix}-${priorityType.id}`;
                    return `
                    <div class="custom-control custom-radio custom-control-inline">
                        <input class="custom-control-input" type="radio" id="${elementId}" name="${prefix}" value="${priorityType.id
                        }"${settings[patchType].priorityType === priorityType.id ? ' checked' : ''}>
                        <label class="custom-control-label" for="${elementId}" data-tippy-content="${priorityType.tooltip
                        }">${priorityType.description}</label>
                    </div>`;
                }

                const prefix = `${id}-${patchType}`;
                const prioritySettings = `${prefix}-prioritysettings`;
                const seedDivs = allSeeds[patchType].map(createSeedDiv).join('');
                return `
                <div id="${prefix}" class="col-12 col-md-6 col-xl-4">
                    <div class="block block-rounded block-link-pop border-top border-farming border-4x" style="padding-bottom: 12px;">
                        <div class="block-header border-bottom">
                            <h3 class="block-title">AutoFarm ${patchType}</h3>
                            <div class="custom-control custom-switch">
                                <input type="checkbox" class="custom-control-input" id="${prefix}-enabled" name="${prefix}-enabled"${settings[patchType].enabled ? ' checked' : ''
                    }>
                                <label class="custom-control-label" for="${prefix}-enabled">Enable</label>
                            </div>
                        </div>
                        <div class="block-content" style="padding-top: 12px">
                            ${Object.values(priorityTypes).map(createPriorityTypeSelector).join('')}
                        </div>
                        <div class="block-content" style="padding-top: 12px">
                            <div id="${prioritySettings}-custom">
                                ${seedDivs}
                                <button id="${prioritySettings}-reset" class="btn btn-primary locked" data-tippy-content="Reset order to default (highest to lowest level)" style="margin: 5px 0 0 2px; float: right;">Reset</button>
                            </div>
                            <div id="${prioritySettings}-mastery" class="${id}-seed-toggles">
                                ${seedDivs}
                            </div>
                            <div id="${prioritySettings}-quantity" class="${id}-seed-toggles">
                                ${seedDivs}
                            </div>
                        </div>
                    </div>
                </div>`;
            }

            const autoFarmDiv = `
            <div id="${id}" class="row row-deck gutters-tiny">
                ${patchTypes.map(createPatchTypeDiv).join('')}
            </div>`;

            $('#farming-container .row:first').after($(autoFarmDiv));

            function addStateChangeHandler(patchType) {
                $(`#${id}-${patchType}-enabled`).change((event) => {
                    settings[patchType].enabled = event.currentTarget.checked;
                    saveSettings();
                    loadFarmingArea(patchTypes.indexOf(patchType));
                });
            }
            patchTypes.forEach(addStateChangeHandler);

            function showSelectedPriorityTypeSettings(patchType) {
                for (const priorityType of Object.values(priorityTypes)) {
                    $(`#${id}-${patchType}-prioritysettings-${priorityType.id}`).toggle(
                        priorityType.id === settings[patchType].priorityType
                    );
                }
            }
            patchTypes.forEach(showSelectedPriorityTypeSettings);

            function lockPatch(patchType, patchId, seedId) {
                if (seedId !== undefined) {
                    settings[patchType].lockedPatches[patchId] = seedId;
                } else {
                    delete settings[patchType].lockedPatches[patchId];
                }
            }

            function addPriorityTypeChangeHandler(patchType) {
                function lockAllPatches(auto = false) {
                    const area = newFarmingAreas[patchTypes.indexOf(patchType)];
                    for (let i = 0; i < area.patches.length; i++) {
                        lockPatch(patchType, i, auto ? undefined : area.patches[i].seedID || -1);
                    }
                    $(`.${id}-seed-selector`).remove();
                    addSeedSelectors();
                }

                $(`#${id} input[name="${id}-${patchType}-prioritytype"]`).change((event) => {
                    if (settings[patchType].priorityType === priorityTypes.replant.id) {
                        lockAllPatches(true);
                    }

                    settings[patchType].priorityType = event.currentTarget.value;
                    if (event.currentTarget.value === priorityTypes.replant.id) {
                        lockAllPatches();
                    }
                    showSelectedPriorityTypeSettings(patchType);
                    saveSettings();
                });
            }
            patchTypes.forEach(addPriorityTypeChangeHandler);

            function makeSortable(patchType) {
                const elementId = `${id}-${patchType}-prioritysettings-custom`;
                Sortable.create(document.getElementById(elementId), {
                    animation: 150,
                    filter: '.locked',
                    onMove: (event) => {
                        if (event.related) {
                            return !event.related.classList.contains('locked');
                        }
                    },
                    onEnd: () => {
                        settings[patchType].priority = [...$(`#${elementId} .${id}-priority-selector`)].map(
                            (x) => +$(x).data('seed-id')
                        );
                        saveSettings();
                    },
                });
            }
            patchTypes.forEach(makeSortable);

            function orderCustomPriorityMenu(patchType) {
                const priority = settings[patchType].priority;
                if (!priority.length) {
                    return;
                }
                const menu = $(`#${id}-${patchType}-prioritysettings-custom`);
                const menuItems = [...menu.children()];

                function indexOfOrInf(el) {
                    let i = priority.indexOf(+el);
                    return i === -1 ? Infinity : i;
                }

                const sortedMenu = menuItems.sort(
                    (a, b) => indexOfOrInf($(a).data('seed-id')) - indexOfOrInf($(b).data('seed-id'))
                );
                menu.append(sortedMenu);
            }

            function addPriorityResetClickHandler(patchType) {
                $(`#${id}-${patchType}-prioritysettings-reset`).on('click', () => {
                    settings[patchType].priority = allSeeds[patchType];
                    orderCustomPriorityMenu(patchType);
                    saveSettings();
                });
            }
            patchTypes.forEach(addPriorityResetClickHandler);

            $(`.${id}-seed-toggles div`).each((_, e) => {
                const toggle = $(e);
                const seedId = toggle.data('seed-id');
                if (settings.disabledSeeds[seedId]) {
                    toggle.css('opacity', disabledOpacity);
                }
            });

            $(`.${id}-seed-toggles div`).on('click', (event) => {
                const toggle = $(event.currentTarget);
                const seedId = toggle.data('seed-id');
                if (settings.disabledSeeds[seedId]) {
                    delete settings.disabledSeeds[seedId];
                } else {
                    settings.disabledSeeds[seedId] = true;
                }
                const opacity = settings.disabledSeeds[seedId] ? disabledOpacity : 1;
                toggle.fadeTo(200, opacity);
                saveSettings();
            });

            patchTypes.forEach((patchType) => {
                orderCustomPriorityMenu(patchType);
                orderMasteryPriorityMenu(patchType);
                orderQuantityPriorityMenu(patchType);
            });

            function createDropdown(patchType) {
                function createDropdownItem(name, icon, seedId) {
                    return `
                    <button class="dropdown-item"${seedId !== undefined ? ` data-seed-id="${seedId}"` : ''
                        } style="outline: none;">
                        <span style="margin-right: 12px; vertical-align: bottom;">${icon}</span>${name}
                    </button>`;
                }

                return `
            <div class="dropdown ${id}-seed-selector">
                <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" style="padding-left: 8px; padding-right: 8px;"><span class="${id}-seed-selector-icon" style="margin-right: 6px; vertical-align: text-bottom; margin-top: 1px;"></span><span class="${id}-seed-selector-text"></span></button>
                <div class="dropdown-menu font-size-sm" style="border-color: #6c757d; border-radius: 0.25rem; padding: 0.25rem 0;">
                    ${createDropdownItem(
                    'Auto',
                    '<img src="assets/media/main/settings_header.svg" width="20" height="20">'
                )}
                    ${allSeeds[patchType]
                        .map((seedId) =>
                            createDropdownItem(
                                items[items[seedId].grownItemID].name,
                                `<img src="${items[items[seedId].grownItemID].media}" width="20" height="20">`,
                                seedId
                            )
                        )
                        .join('')}
                    ${createDropdownItem(
                            'None',
                            '<i class="fa fa-ban" style="width: 20px; font-size: 20px; color: #c81f1f; vertical-align: middle;"></i>',
                            -1
                        )}
                </div>
            </div>`;
            }

            function addSeedSelectors() {
                function updateDropdownSelection(patchType, patchId, dropdown) {
                    dropdown.find('.dropdown-item.active').removeClass('active');
                    const button = dropdown.children('button');
                    const selectedSeed = settings[patchType].lockedPatches[patchId];
                    let selected;
                    if (selectedSeed !== undefined) {
                        selected = dropdown.find(`.dropdown-item[data-seed-id="${selectedSeed}"]`);
                        button.find(`.${id}-seed-selector-text`).text('');
                    } else {
                        selected = dropdown.find('.dropdown-item:not([data-seed-id])');
                        button.find(`.${id}-seed-selector-text`).text('Auto');
                    }
                    selected.addClass('active');
                    button.find(`.${id}-seed-selector-icon`).html(selected.find('span').html());
                }

                $('#farming-area-container h3').each((patchId, e) => {
                    const header = $(e);
                    if (header.siblings(`.${id}-seed-selector`).length) {
                        // Seed selector already exists
                        return;
                    }
                    const patchType = toPatchType[header.text().toUpperCase()];
                    if (patchType === undefined) {
                        // Locked patch
                        return;
                    }

                    if (settings[patchType].enabled) {
                        // Remove game's seed picker
                        header.siblings('.block-options').remove();
                    }

                    const dropdown = $(createDropdown(patchType, patchId));
                    updateDropdownSelection(patchType, patchId, dropdown);

                    dropdown.find('.dropdown-item').on('click', (event) => {
                        lockPatch(patchType, patchId, $(event.currentTarget).data('seed-id'));
                        saveSettings();
                        updateDropdownSelection(patchType, patchId, dropdown);
                    });

                    header.after(dropdown);
                });
            }
            addSeedSelectors();

            if (observer) {
                observer.disconnect();
            }
            observer = new MutationObserver(addSeedSelectors);
            observer.observe(document.getElementById('farming-area-container'), { childList: true });

            tippy(`#${id} [data-tippy-content]`, { animation: false, allowHTML: true });
        }

        injectGUI();
        setInterval(autoFarm, 15000);
    }

    // function removeGUI() {
    //     if (observer) {
    //         observer.disconnect();
    //     }
    //     $(`#${id} [data-tippy-content]`).each((_, e) => e._tippy.destroy());
    //     $(`#${id}`).remove();
    //     $(`.${id}-seed-selector`).remove();
    // }

    function loadScript() {
        if (typeof confirmedLoaded !== 'undefined' && confirmedLoaded) {
            clearInterval(interval);
            console.log('Loading AutoFarm');
            startAutoFarm();
        }
    }

    const interval = setInterval(loadScript, 1000);
});