Krapotor / Pet Farming enhancement

// ==UserScript==
// @name         Pet Farming enhancement
// @namespace    https://openuserjs.org/users/Krapotor
// @version      1.4
// @description  This scripts adds more advanced feature like planning and state export/import to the nice Pet Farming page for the Endless Frontier RPG game available on https://jared-g.github.io/pet-farming/
// @author       Krapotor (Hunters), S8
// @match        https://jared-g.github.io/pet-farming/
// @grant        none
// @license      LGPL-3.0
// @copyright    2018, Krapotor (https://openuserjs.org/users/Krapotor)
// @updateURL    https://openuserjs.org/meta/Krapotor/Pet_Farming_enhancement.meta.js
// @installURL   https://openuserjs.org/install/Krapotor/Pet_Farming_enhancement.user.js
// ==/UserScript==

// ==OpenUserJS==
// @author       Krapotor
// ==/OpenUserJS==

(function() {
    'use strict';


    // replace the existing function
    function updateDaysRemaining() {
        updatePlanning();
        for(let i = 0; i < petList.length; ++i) {
            const pet = petList[i];
            const farmString = pet.duration > 0 ? "&nbsp;" + (pet.start > 1 ? pet.start +" + " : "") + pet.duration+" days" : "&nbsp;";
            $($(".petinput span").get(i)).html(farmString);
        }
    }


    function getMaxFragmentsPerDay(pet) {
        var kltest = knightLevel % 2 === 0 ? knightLevel + 1 : knightLevel;
        //console.log(pet.reqs)
        return pet.reqs.reduce((acc, req) => {
            if (req <= kltest) {
                if(req % 2 === 0) {
                    acc += 1;
                } else {
                    acc += 3;
                }}
            return acc;
        }, 0);
    }

    function updatePlanning() {
        const petByName = petList.reduce((acc, value) => {acc[value.name] = value; return acc; }, {});
        const nbEntriesPerDay = entries + (refills * 5);
        const dailyUse = [];
        let missingPetsDueToLowKL = false;
        for(let name of tierList) {
            //console.log(name)
            const pet = petByName[name];
            const maxFragsPerDay = getMaxFragmentsPerDay(pet);
            if (maxFragsPerDay === 0) {
                // pet not available yet according to current KL level
                pet.duration = Infinity;
                pet.start = Infinity;
                missingPetsDueToLowKL = true;
                continue;
            }
            let remFrags = 330 - pet.fragments;
            // update frags consumption
            let startDay = -1;
            let i = 0;
            while (remFrags > 0) {
                if (!dailyUse.hasOwnProperty(i)) {
                    dailyUse[i] = { nb: 0};
                }
                let use = dailyUse[i];
                if (use.nb < nbEntriesPerDay) {
                    if (startDay === -1) {
                        startDay = i;
                    }
                    const usedFrags = Math.min(maxFragsPerDay, remFrags, nbEntriesPerDay-use.nb);
                    use.nb += usedFrags;
                    if (!use.hasOwnProperty('pets')) {
                        use.pets = {};
                    }
                    if (!use.pets.hasOwnProperty(name)) {
                        use.pets[name] = 0;
                    }
                    use.pets[name] += usedFrags;
                    remFrags -= usedFrags;
                }
                i++;
            }
            if (startDay === -1) {
                pet.duration = 0;
                pet.start = -1;
            } else {
                pet.duration = i - startDay;
                pet.start = startDay + 1;
            }
            //if (pet.duration > 0) {
            //    console.log(name+": "+pet.duration+" days (start in " + pet.start +" days)")
            //}
        }

        // check
        {
            let lastNb = nbEntriesPerDay;
            for(let use of dailyUse) {
                if(lastNb < use.nb) {
                    throw 'lastNb: ' + lastNb + " < " + use.nb;
                }
                lastNb = use.nb;
                const total = Object.values(use.pets).reduce((acc, val) => acc + val);
                if (total !== use.nb) {
                    throw 'total: '+total+ ' != ' + use.nb;
                }
            }
        }

        // display planning
        updatePlanningDiv(dailyUse, petByName);
        updatePlanningSummaryDiv(dailyUse, nbEntriesPerDay, missingPetsDueToLowKL);
    }

    function updatePlanningDiv(dailyUse, petByName) {
        let $planning = $('#planning');
        $planning.empty();
        for (let i = 0; i < dailyUse.length; ++i) {
            const use = dailyUse[i];
            let str = '<div>Day ' + (i+1) + ': ' + use.nb + ' fragments ';
            for (let name in use.pets) {
                str += '<a href="https://www.endlessfrontierdata.com/pets/'+ petByName[name].img +'" style=""><img class="icon-32" src="files/img/' + petByName[name].img + '.png" style="opacity: 1;" alt="'+ name +'" title="'+ name +'"></a>x' + use.pets[name] + ' ';
            }
            str += '</div>';
            $planning.append(str);
        }
    }

    function updatePlanningSummaryDiv(dailyUse, nbEntriesPerDay, missingPetsDueToLowKL) {
        let $summary = $('#planning-summary');
        $summary.empty();
        $summary.text("Duration: " + (dailyUse.length)+ " days");
        if (missingPetsDueToLowKL) {
            $summary.append('<br/><span style="color: red; font-variant: italic;">Planning deosn\'t include all the required pets since they are not avalaible yet. Increase your knight level to be able to farm those pets.</span>');
        }
        let i = dailyUse.length - 2;
        if (i >= 2 && dailyUse[i-2].nb < nbEntriesPerDay) {
            $summary.append('<br/><span style="color: red; font-variant: italic;">Planning is likely sub-optimal. You should re-order the pets in the tier list or increase your knight level to farm more fragments of the last pets</span>');
        }
    }

    function restoreState() {
        const rawData = $('#rawState').val();
        const data = JSON.parse(rawData);
        Object.keys(data).forEach(k => localStorage.setItem(k, data[k]));

        // needed since updatePetList reads value from here
        $("#input-kl").val(data.knightLevel);
        $("#input-entries").val(data.entries);
        $("#input-refills").val(data.refills);
        updatePetList();
    }
    function dumpState() {
        const state = JSON.stringify(localStorage);
        $('#rawState').val(state);
    }

    //$('#petinputcontainer').parent().children().slice(-2).remove()
    $('#petinputcontainer').parent().append('<div style="margin-top: 1em; margin-left: 4px; display: inline-block;"><h2>Complete planning</h2></div><div style="margin: 4px; color: #bbb; background: #333; border: 1px solid #555; display: inline-block;"><div id="planning" style="max-height:300px; overflow: auto; padding: 2px;"></div><div id="planning-summary" style="margin-top: 2px; padding: 2px; border-top: 1px solid #555"></div></div><div style="margin: 4px; margin-top: 2em; display: inline-block;"><h2>Use this section to save/restore the state</h2><textarea id="rawState" style="width: auto; min-width: 600px; min-height: 100px;"></textarea><br/><button id="copyState" onclick="dumpState()" style="width: 100px; margin: 2px;">Get state</button><button id="restoreState" onclick="restoreState()" style="width: 100px; margin: 2px;">Restore state</button></div>');

    // initialize
    updateDaysRemaining();

    // export global functions
    window.updateDaysRemaining = updateDaysRemaining;
    //window.updatePlanning = updatePlanning;
    window.dumpState = dumpState;
    window.restoreState = restoreState;
})();