IdleScape - Lootify

IdleScape Statistics Tracker

当前为 2021-02-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         IdleScape - Lootify
// @namespace    D4IS
// @version      1.4
// @description  IdleScape Statistics Tracker
// @author       D4M4G3X
// @match        *://*.idlescape.com/*
// @grant        none
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// @run-at document-start
// ==/UserScript==

(function() {

    /* INITIATE APPLICATION */
    let lib = {};
    let app = {};
    let initInterval = setInterval(()=> {
        if (window.D4IS) {
            clearInterval(initInterval);
            lib = window.D4IS.init('lootify', '1.4');
            app = lib['lootify'];
            lib.app.setUpdateLocation(lib.game.getMenuItem('Lootify Settings'));
            main();
        }
    }, 100);

    function main() {
        /* SET DEFAULT VALUES */
        app.runs = [{}];
        let total = {};
        let totalLoot = {};
        let headers = {};

        /* ############## INTERVALS ############## */
        let mainInterval = setInterval(()=> {
            if (app.ready && $('.status-bar').length) {
                if(is()) {
                    if(!app.setup) {
                        app.setup = true;
                        setupSettings();
                        lib.app.updateUser(app.name);
                        app.getKills();
                        setupLogs();
                    }
                    renderErrors();
                    renderStatus();
                    saveLogs();
                    mergeLogs();
                    renderLogs();
                    renderTimer();
                    renderKPH();
                    renderGPH();
                    renderXPH();
                }
            }
        }, 500);

        let pasteInterval = setInterval(()=> {
            if (app.ready && $('.status-bar').length) {
                if(lib.general.getStorage('AutoPaster') == 'true') {
                    reset('single');
                }
            }
        }, getPasteInterval() * 60 * 1000);

        let adInterval = setInterval(()=> {
            if (app.ready && $('.status-bar').length) {
                lib.game.chat({
                    'msg': 'Lootify - Please report bugs to D4M4G3X#6263 on Discord!',
                    'color': '#00a0fd',
                });
            }
        }, 20 * 60 * 1000);

        let updateInterval = setInterval(()=> {
            if (app.ready && $('.status-bar').length) {
                lib.app.getData();
                lib.app.updateUser(app.name);
                app.getKills();
            }
        }, 15 * 60 * 1000);

        let quickInterval = setInterval(()=> {
            if (app.ready && $('.status-bar').length) {
                addConfirmations();
                checkRemovals();
            }
        }, 100);

        function addConfirmations() {

            // #### GATHERING CONFIRMATIONS #### //
            $('.resource-container-button').each(function() {
                if(lib.app.data) {
                    if (!lib.app.data.active.skills.includes($(this).parents('.play-area').attr('class').split('theme-')[1])) {
                        return false;
                    }

                    let $parent = $(this).parents('.resource-container');
                    if( !$(this).next('.resource-button-overlay').length && !~$(this).text().indexOf('Stop')) {
                        let $btn = $(this).find('.resource-button').hide();

                        let $btnClone = $('<div>', {
                            'class': 'resource-button-overlay resource-button'
                        }).text($btn.text()).insertAfter($(this));

                        $btnClone.click(function() {
                            let includes = false;
                            // Game Start
                            if ((lib.user.isStatus('idling') && !app.prevStatus) || isLogEmpty()) {
                                $parent.find('.resource-button:not(.resource-button-overlay)').click();
                                return false;
                            }

                            // Is Logged
                            if (isLogged('skill', $parent.find('.resource-container-title').text())) {
                                $parent.find('.resource-button:not(.resource-button-overlay)').click();
                                return false;
                            }

                            lib.game.dialog({
                                'title': 'Lootify warning!',
                                'text': 'Going to a different location will paste and reset your Lootify statistics, are you sure you want to do this?',
                                'img': true,
                                'cbyes': function() {
                                    reset();
                                    $parent.find('.resource-button:not(.resource-button-overlay)').click();
                                }
                            });
                        });
                    }
                    if (~$(this).text().indexOf('Stop')) {
                        $(this).next('.resource-button-overlay').remove();
                        $(this).find('.resource-button').show();
                    }
                }
            });

            // #### COOKING CONFIRMATIONS #### //

            if ($('.cooking-item').find('img').length) {
                if(!$('.cooking-start-clone').length && !lib.user.isStatus('cooking')) {
                    let $cookBtn = $('.cooking-start-button').css('visibility', 'hidden');
                    let $cloneBtn = $('<div>', {
                        'class': 'cooking-start-button cooking-start-clone'
                    }).text('Start Cooking').insertAfter($cookBtn);
                    $(window).resize(function() {
                        let pos = $cookBtn.position();
                        $cloneBtn.width($cookBtn.width());
                        $cloneBtn.css({
                            'position': 'absolute',
                            'z-index': '1000',
                            'top': pos.top,
                            'left': pos.left,
                        });
                    }).resize();
                    $cloneBtn.click(function() {

                        // Game Start
                        if ((lib.user.isStatus('idling') && !app.prevStatus) || isLogEmpty()) {
                            $cookBtn.click();
                            $cookBtn.css('visibility', 'visible');
                            $cloneBtn.hide();
                            return false;
                        }

                        // Is Previous Status
                        if (lib.user.isPrevStatus('cooking')) {
                            $cookBtn.click();
                            $cookBtn.css('visibility', 'visible');
                            $cloneBtn.hide();
                            return false;
                        }

                        lib.game.dialog({
                            'title': 'Lootify warning!',
                            'text': 'Cooking now will paste and reset your current Lootify statistics, are you sure you want to do this?',
                            'img': true,
                            'cbyes': function() {
                                reset();
                                $cloneBtn.hide();
                                $cookBtn.css('visibility', 'visible');
                                $cookBtn.click();
                            }
                        });
                    });
                }
            } else {
                $('.cooking-start-button').css('visibility', 'visible');
                $('.cooking-start-clone').remove();
            }

            // #### COMBAT CONFIRMATIONS #### //

            if ($('.combat-zones').length && !$('.zone-button-overlay').length) {
                let $overlay = $('<div>', {
                    'class': 'zone-button-overlay'
                }).appendTo($('.combat-zones'));

                $('.combat-zone').each(function() {
                    let $zoneBtn = $('<div>', {
                        'class':'combat-zone-clone'
                    }).text($(this).text()).appendTo($overlay);

                    $zoneBtn.click(function(e) {
                        let $parent = $(this).parents('.combat-zones');
                        let $that = $(this);
                        let includes = false;

                        // Game Start
                        if ((lib.user.isStatus('idling') && !app.prevStatus) || isLogEmpty()) {
                            $parent.find('.combat-zone').each(function() {
                                if($(this).text() === $that.text()) {
                                    $(this).click();
                                }
                            });
                            return false;
                        }

                        // Is Logged
                        if (isLogged('combat', $(this).text())) {
                            $parent.find('.combat-zone').each(function() {
                                if($(this).text() === $that.text()) {
                                    $(this).click();
                                }
                            });
                            return false;
                        }

                        // Is an event mob
                        if (lib.game.isEventMob(app.currentMob)) {
                            $parent.find('.combat-zone').each(function() {
                                if($(this).text() === $that.text()) {
                                    $(this).click();
                                }
                            });
                            return false;
                        }

                        lib.game.dialog({
                            'title': 'Lootify warning!',
                            'text': 'Going to a different zone will paste and reset your Lootify statistics, are you sure you want to do this?',
                            'img': true,
                            'cbyes': function() {
                                $parent.find('.combat-zone').each(function() {
                                    if($(this).text() === $that.text()) {
                                        reset();
                                        $(this).click();
                                    }
                                });
                            },
                        });
                    });
                });

                $(window).resize(function() {
                    $overlay.width($('.combat-zones').width());
                }).resize();
            }
        }
        function checkRemovals() {
            if(!lib.user.isStatus('foraging')) { return; }
            if ($('.recipe-item').length) {
                $('.recipe-item').each(function() {
                    if($(this).find('img').attr('src') === '/images/foraging/log.png') {
                        $(this).remove();
                    }
                });
            }
        }

        // #### SETUP SETTINGS ####
        function setupSettings() {
            if(!lib.game.getMenuItem('Lootify', 'category')) {
                lib.game.setMenuItem({
                    'text': 'Lootify',
                    'clone': 'Gathering',
                    'class': 'hdr-lootify',
                    'before': lib.game.getMenuItem('Gathering', 'category'),
                }, 'category');
                $('<i/>').text(' (v'+app.ver+')').appendTo(lib.game.getMenuItem('Lootify', 'category'));
            }
            // ### PRIVACY SETTINGS ###
            lib.game.addSetting({
                'app': 'lootify',
                'text': 'Allow Lootify to publicly use your username for personal highscores and kill statistics',
                'name': 'UserPublic',
                'type': 'checkbox',
                'default': 'false',
                'change': function() {
                    setUserPublic($(this).prop('checked'));
                    lib.general.setStorage('UserPublic', $(this).prop('checked'));
                }
            });
            if (!lib.general.getStorage('Terms')) {
                let msg = "";
                msg += "By using Lootify you agree to allow us saving your username and ID in our database for statistical purposes.";
                msg += "These will be used for setting up personal statistics and highscores. (coming soon...) ";
                msg += "If you don't want your username to be on public highscore lists then you can opt-out in the settings";
                lib.game.dialog({
                    'title': 'Terms of service',
                    'text': msg,
                    'type': 'confirm',
                    'img': true,
                    'cbconfirm': function() {
                        lib.general.setStorage('Terms', 'true');
                    },
                });
            }
            // ### SETTING AUTO PASTER ###
            lib.game.addSetting({
                'app': 'lootify',
                'text': 'Enable Auto Paster',
                'name': 'AutoPaster',
                'type': 'checkbox',
                'default': 'false',
                'change': function() {
                    lib.general.setStorage('AutoPaster', $(this).prop('checked'));
                }
            });
            // ### SETTING PASTE INTERVAL ###
            lib.game.addSetting({
                'app': 'lootify',
                'text': 'Paste Interval (minutes)',
                'name': 'AutoPasterInterval',
                'type': 'number',
                'min': 15,
                'max': 60,
                'default': 30,
                'change': function() {
                    if($(this).val() >= $(this).attr('min') && $(this).val() <= $(this).attr('max')) {
                        lib.general.setStorage('AutoPasterInterval', $(this).val());
                    }
                }
            });
            // ### SETTING CHAT MESSAGES ###
            lib.game.addSetting({
                'app': 'lootify',
                'text': 'Enable Chat Messages',
                'name': 'ChatMessage',
                'type': 'checkbox',
                'default': 'true',
                'change': function() {
                    lib.general.setStorage('ChatMessage', $(this).prop('checked'));
                }
            });
        }
        function renderErrors() {
            if (!app.disabled || !app.enabled) {
                $('.lootify-error').remove();
            }
            if (!$('.lootify-error.error-paster').length && app.disabled) { // TODO: Create Error Message Function
                let $error = $('<div/>', {
                    'class': 'lootify-error error-paster'
                }).css({
                    'color': 'red',
                    'width': '80%',
                    'margin': '0 auto',
                }).html('Paster is disabled!<br>Reset log to Enable again.').insertAfter(lib.game.getMenuItem('Lootify', 'category'));
                lib.game.tooltip($error, 'Pasting is disabled on market usage, you can still use the counters.<br> This is done to prevent skewing the statistics.');
            }
            if (!$('.lootify-error.error-disabled').length && (app.enabled && app.enabled === 'false')) {  // TODO: Create Error Message Function
                $('<div/>', {
                    'class': 'lootify-error error-disabled'
                }).css({
                    'color': 'red',
                    'width': '80%',
                    'margin': '0 auto',
                }).html('Paster is globally disabled!').insertAfter(lib.game.getMenuItem('Lootify', 'category'));
            }
        }
        function renderStatus() {
            if (!lib.user.isFighting() && !lib.user.isActiveSkill() && !lib.user.isStatus('idling')) {
                $('.ltf-status').remove();
                return false;
            }
            if (!lib.game.getMenuItem('Status:')) {
                lib.game.setMenuItem({
                    'class': 'ltf-status',
                    'text': 'Status: ' + lib.user.getStatus(),
                    'icon': '/images/combat/combat_level.png',
                    'after': lib.game.getMenuItem('Lootify', 'category'),
                    'click': function() {
                        $('[data-for="'+lib.user.getStatus()+'Header"]').click();
                    }
                });
            }
            let status = lib.user.getStatus();
            status += lib.user.isFighting() ? ' '+lib.user.currentMob : '';
            $('.ltf-status').find('span').text('Status: ' + lib.general.ucfirst(status));
        }
        function setupLogs() {
            if ($('.btn-loot-log').length) {
                $('.btn-loot-log').remove();
            }
            let $logBtn = lib.game.getMenuItem('Loot Log');
            if (($logBtn && $logBtn.text() === 'Loot Log') && !$logBtn.hasClass('btn-loot-log')) {
                lib.game.editMenuItem('Loot Log');
            }
            if(!lib.game.getMenuItem('Loot Log')) {
                lib.game.setMenuItem({
                    'icon': '/images/ui/inventory_icon.png',
                    'text': 'Loot Log',
                    'class': 'btn-loot-log',
                    'before': lib.game.getMenuItem('Gathering', 'category'),
                    'click': function() {
                        $('.item-log-clone').toggle();
                    },
                });
            }
            let $logwrap = $('.item-log-window');
            $logwrap.hide();
            let $clone = $logwrap.clone();
            $clone.addClass('item-log-clone').removeClass('hidden');
            $clone.find('.item-log-timer').remove();
            $clone.find('.item-log-info').remove();
            $clone.find('.drawer-setting-large').addClass('clone').unbind().click(function() {
                reset();
            }).text('Paste and Reset');
            if (app.disabled || lib.user.isStatus('fishing') || lib.user.isStatus('smithing') || lib.user.isStatus('cooking')) {
                $clone.find('.drawer-setting-large').text('Reset');
            }
            lib.game.getMenuItem('Loot Log').after($clone);
        }
        function renderTimer() {
            if (!lib.user.isFighting() && !lib.user.isActiveSkill() && !lib.user.isStatus('idling')) {
                $('.time-stats').remove();
                return false;
            }
            if (!lib.game.getMenuItem('Elapsed:')) {
                let $timeStats = lib.game.setMenuItem({
                    'class': 'time-stats',
                    'text': 'Elapsed: 0S',
                    'icon': '/images/clock.png',
                    'after': $('.ltf-status')
                });
            }
            setTimeout(()=> {
                !lib['lootify'].timers ? lib['lootify'].timers = {} : 1;
                if(!lib['lootify'].timers[lib.user.getStatus()]) {
                    new lib.general.Timer(app.name, lib.user.getStatus());
                }
            }, 100);
        }
        function renderKPH() {
            if (!lib.user.isFighting()) {
                $('.kph-stats').remove();
                $('.kph-wrap').remove();
                return false;
            }
            if (!lib.game.getMenuItem('Kills:')) {
                let $kphStats = lib.game.setMenuItem({
                    'class': 'kph-stats',
                    'text': 'Kills: 0 p/h',
                    'icon': '/images/combat/combat_level.png',
                    'after': $('.time-stats'),
                    'click': function() {
                        $('.kph-wrap').toggle();
                    }
                });
                $kphStats.after($('<div/>', {
                    'class': 'kph-wrap ltf-submenu'
                }).hide());
            }
            let mobkills = {};
            $.each(total, function(mob, mobinfo) {
                mobkills[mob] = mobinfo.count;
            });
            let totalkills = function() {
                let c = 0;
                $.each(mobkills, function(mob, kills) {
                    c += kills;
                });
                return c;
            };
            let kph = 0;
            let time = lib.general.getTimerTime('lootify', lib.user.getStatus());
            kph = lib.general.addCommas(Math.floor((totalkills()/time)*3600));
            $('.kph-stats').find('span').text('Kills: ' + kph + ' p/h');
            $('.kph-wrap').empty();
            $.each(lib.general.sortObject(mobkills), function(mob, count) {
                let $mobwrap = $('<div/>', {
                    'class': 'ltf-header mob-header'
                }).appendTo($('.kph-wrap'));
                kph = lib.general.addCommas(Math.floor((count/time)*3600));
                let $mobinfo = $('<span/>').css('display','block').text(mob + ': ' + kph + ' p/h');
                $mobinfo.prepend($('<img>').attr('src', lib.game.getMobIcon(mob)));
                $mobwrap.append($mobinfo);
            });
        }
        function renderGPH() {
            if((!lib.user.isActiveSkill() && !lib.user.isFighting()) || lib.user.isIron() || lib.user.isStatus('cooking')) {
                $('.gph-stats').remove();
                $('.gph-wrap').remove();
                return false;
            }
            if (!lib.game.getMenuItem('Gold:')) {
                let $gphStats = lib.game.setMenuItem({
                    'class': 'gph-stats',
                    'text': 'Gold: 0 p/h',
                    'icon': '/images/ui/shop_icon.png',
                    'after': $('.time-stats'),
                    'click': function() {
                        $('.gph-wrap').toggle();
                    }
                });
                $gphStats.after($('<div/>', {
                    'class': 'gph-wrap ltf-submenu'
                }).hide());
            }

            let totalGold = 0;
            if (lib.game.marketPrices) {
                $.each(totalLoot, function(item, count) {
                    if (item === 'Gold') {
                        totalGold += count;
                    } else {
                        if (lib.user.isStatus('smithing')) {
                            let ore = item.split(' ')[0] + ' Ore';
                            totalGold += (lib.game.marketPrices[item] - lib.game.marketPrices[ore])* count;
                        } else {
                            totalGold += lib.game.marketPrices[item] * count;
                        }
                    }
                });
            } else {
                totalGold = 'n/a';
            }

            let time = lib.general.getTimerTime('lootify', lib.user.getStatus());
            if (time) {
                let gph = lib.general.addCommas(Math.floor((totalGold/time)*3600));
                $('.gph-stats').find('span').text('Gold: ' + gph + ' p/h');
            }
            $('.gph-wrap').empty();
            let $lootwrap = $('<div/>', {
                'class': 'ltf-header mob-header'
            }).appendTo($('.gph-wrap'));

            let lootval = lib.general.addCommas(Math.floor(totalGold));
            let $lootInfo = $('<span/>').css('display','block').text('Loot value: ' + lootval);
            $lootInfo.prepend($('<img>').attr('src', '/images/money_icon.png'));
            $lootwrap.append($lootInfo);
        }
        function renderXPH() {
            if(!lib.user.isActiveSkill() && !lib.user.isFighting()) {
                $('.mxph-stats').remove();
                $('.mxph-wrap').remove();
                $('.xph-stats').remove();
                $('.xph-wrap').remove();
                return false;
            }
            let currentLevel = {};
            let approxTime = {};
            let totalExp = {};
            let skillExp = {};
            let combatSkills = ['attack', 'defense', 'strength', 'constitution'];
            let expTypes = ['exp', 'mexp'];
            let time = lib.general.getTimerTime('lootify', lib.user.getStatus());
            !lib.user.levelGoal ? lib.user.levelGoal = {} : 1;
            $.each(expTypes, function(k, type) {
                if (lib.user[type]) {
                    // CALCULATE EXP FOR COMBAT SKILLS
                    if (lib.user.isFighting()) {
                        $.each(combatSkills, function(k, skill) {
                            !skillExp[type] ? skillExp[type] = [] : 1;
                            !totalExp[type] ? totalExp[type] = 0 : 1;
                            !currentLevel[type] ? currentLevel[type] = [] : 1;
                            // GET THE CURRENT SKILL EXP GAINED
                            skillExp[type][skill] = lib.user[type][skill].current - lib.user[type][skill].init;
                            // GET TOTAL EXP GAINED OF ALL COMBAT SKILLS
                            totalExp[type] += (lib.user[type][skill].current - lib.user[type][skill].init);
                            currentLevel[type][skill] = lib.game.getLevel(lib.user[type][skill].current);
                            !lib.user.levelGoal[skill] ? lib.user.levelGoal[skill] = currentLevel[type][skill] + 1 : 1;
                        });
                    }
                    // CALCULATE EXP FOR GATHERING SKILLS
                    $.each(lib.app.data.active.skills, function(k, skill) {
                        if(lib.user.isStatus(skill)) {
                            totalExp[type] = lib.user[type][skill].current - lib.user[type][skill].init;
                            currentLevel[type] = lib.game.getLevel(lib.user[type][skill].current);
                            !lib.user.levelGoal[skill] ? lib.user.levelGoal[skill] = currentLevel[type] + 1 : 1;
                        }
                    });
                }
            });
            // EXP COUNTERS
            $.each(expTypes, function(k, type) {
                let menuItem = (type === 'exp') ? 'Experience:' : 'Mastery:';
                let menuClass = (type === 'exp') ? 'xph' : 'mxph:';
                let menuIcon = (type === 'exp') ? '/images/total_level.png' : '/images/total_level_mastery_icon.png';

                if (!lib.game.getMenuItem(menuItem) && totalExp[type] > 1) {
                    let $xphStats = lib.game.setMenuItem({
                        'class': menuClass+'-stats',
                        'text': menuItem+' 0 p/h',
                        'icon': menuIcon,
                        'after': $('.time-stats'),
                        'click': function() {
                            $('.'+menuClass+'-wrap').toggle();
                        }
                    });
                    $xphStats.after($('<div/>', {
                        'class': menuClass+'-wrap ltf-submenu'
                    }).hide());
                }

                let xph = lib.general.addCommas(Math.floor((totalExp[type]/time)*3600));
                $('.'+menuClass+'-stats').find('span').text(menuItem + ' ' + xph + ' p/h');
                $('.'+menuClass+'-wrap').empty();


                // XP Earned since activity start
                let xpearned = lib.general.addCommas(Math.floor(totalExp[type]));
                let $expwrap = $('<div/>', {
                    'class': 'ltf-header mob-header'
                });
                $expwrap.appendTo($('.'+menuClass+'-wrap'));
                let $expEarnedInfo = $('<span/>').css('display','block').text('Exp earned: ' + xpearned);
                $expEarnedInfo.prepend($('<img>').attr('src', 'https://digimol.net/idlescape/assets/img/plus_sign.png'));
                $expwrap.append($expEarnedInfo);

                // Time Left For Next Level
                let $expTimeWrap, $expTimeInfo;
                if (lib.user[type]) {
                    if(!lib.user.isFighting()) {
                        let skill = lib.user.getStatus();
                        approxTime = lib.user.getTimeToLevel(lib.user.levelGoal[skill], lib.user[type][skill].current, totalExp[type], time);
                        $expTimeWrap = $expwrap.clone().empty().appendTo($('.'+menuClass+'-wrap'));
                        $expTimeInfo = $('<div>').css('display','flex')
                        $expTimeInfo.append($('<img>').attr('src', lib.game.getSkillIcon(skill)));
                        $expTimeInfo.append($('<span>').text('to '+lib.user.levelGoal[skill]));
                        let $arrows = $('<div>', {
                            'class': 'd4is-control-arrows'
                        }).appendTo($expTimeInfo);
                        $('<span>').text('▲').click(function() {
                            if(lib.user.levelGoal[skill] < 200) {
                                lib.user.levelGoal[skill]++;
                            }
                        }).appendTo($arrows);
                        $('<span>').text('▼').click(function() {
                            if(lib.user.levelGoal[skill] > currentLevel[type]+1) {
                                lib.user.levelGoal[skill]--;
                            }
                        }).appendTo($arrows);
                        $expTimeInfo.append($('<span>').text(approxTime));
                        $expTimeWrap.append($expTimeInfo);
                    } else {
                        $.each(combatSkills, function(k, skill) {
                            if(skillExp[type][skill] > 0) {
                                approxTime = lib.user.getTimeToLevel(lib.user.levelGoal[skill], lib.user[type][skill].current, skillExp[type][skill], time);
                                $expTimeWrap = $expwrap.clone().empty().appendTo($('.'+menuClass+'-wrap'));
                                $expTimeInfo = $('<div>').css('display','flex');
                                $expTimeInfo.append($('<img>').attr('src', lib.game.getSkillIcon(skill)));
                                $expTimeInfo.append($('<span>').text('to '+lib.user.levelGoal[skill]));
                                let $arrows = $('<div>', {
                                    'class': 'd4is-control-arrows'
                                }).appendTo($expTimeInfo);
                                $('<img>').attr('src', 'https://digimol.net/idlescape/assets/img/arrow_up.png').click(function() {
                                    if(lib.user.levelGoal[skill] < 200) {
                                        lib.user.levelGoal[skill]++;
                                    }
                                }).appendTo($arrows);
                                $('<img>').attr('src', 'https://digimol.net/idlescape/assets/img/arrow_down.png').click(function() {
                                    if(lib.user.levelGoal[skill] > currentLevel[type][skill]+1) {
                                        lib.user.levelGoal[skill]--;
                                    }
                                }).appendTo($arrows);
                                $expTimeInfo.append($('<span>').text(approxTime));
                                $expTimeWrap.append($expTimeInfo);
                            }
                        });
                    }
                }
            });
        }
        function saveLogs() {
            if (!lib.user.isFighting()) { return false; }
            let $logcats = $('.item-log-window:not(.item-log-clone)').find('.item-log-cateogry');
            if ($logcats.length) {
                $logcats.each(function() {
                    if( $(this).find('.item-log-category-closed').length ) {
                        $(this).find('.item-log-category-closed').click();
                    }
                    let mobs = $(this).find('.item-log-category-open').text().split(" x ");
                    !app.runs[0][mobs[0]] ? app.runs[0][mobs[0]] = {} : 1;
                    app.runs[0][mobs[0]].count = parseInt(mobs[1]);
                    $(this).find('.item-log-item').each(function() {
                        if ($(this).text() !== "None") {
                            let loot = $(this).text().split(" x ");
                            !app.runs[0][mobs[0]].loot ? app.runs[0][mobs[0]].loot = {} : 1;
                            !app.runs[0][mobs[0]].loot[loot[0]] ? app.runs[0][mobs[0]].loot[loot[0]] = {} : 1;
                            app.runs[0][mobs[0]].loot[loot[0]].count = parseInt(loot[1]);
                        }
                    });
                });
            }
        }
        function mergeLogs() {
            if(app.runs) {
                total = {};
                totalLoot = {};
                $.each(app.runs, function(num, run) {
                    $.each(run, function(mobname, mobinfo) {
                        !total[mobname] ? total[mobname] = {} : 1;
                        !total[mobname].count ? total[mobname].count = 0 : 1;
                        total[mobname].count += mobinfo.count;
                        $.each(mobinfo.loot, function(lootname, lootinfo) {
                            /* SET TOTAL LOOT PER MOB */
                            !total[mobname].loot ? total[mobname].loot = {} : 1;
                            !total[mobname].loot[lootname] ? total[mobname].loot[lootname] = {} : 1;
                            !total[mobname].loot[lootname].count ? total[mobname].loot[lootname].count = 0 : 1;
                            !total[mobname].loot[lootname].procs ? total[mobname].loot[lootname].procs = 0 : 1;
                            total[mobname].loot[lootname].count += lootinfo.count;
                            total[mobname].loot[lootname].procs += lootinfo.procs;
                            /* SET TOTAL LOOT OVERALL */
                            let procs = lootinfo.procs ? lootinfo.procs : 0;
                            !totalLoot[lootname] ? totalLoot[lootname] = 0 : 1;
                            totalLoot[lootname] += lootinfo.count + procs;
                        });
                    });
                });
                delete total[''];
            }
        }
        function renderLogs() {
            if(!app.runs) { return false; }
            let $logEntry, $logHeader, $lootWrap, $lootEntry;
            $('.item-log-clone').find('.item-log-cateogry').remove();
            $.each(lib.general.sortObject(total), function(mob, mobinfo) {
                if (!$('.log-entry-'+mob.toLowerCase().split(' ').join('_')).length) {
                    // Log Wrapper
                    $logEntry = $('<div/>', {
                        'class':'ltf-log-entry log-entry-'+mob.toLowerCase().split(' ').join('_')
                    });
                    $logHeader = $('<div/>', {
                        'class':'ltf-log-header noselect'
                    }).appendTo($logEntry);

                    $lootWrap = $('<div/>', {
                        'class': 'ltf-loot-wrap'
                    }).appendTo($logEntry);

                    $logHeader.click(function() {
                        headers[mob] = headers[mob] == false ? true : false;
                        $lootWrap.toggle();
                    });
                    if (lib.user.isStatus('smithing')) {
                        $.each(mobinfo.loot, function(loot, info) {
                            if(~loot.indexOf('Bar')) {
                                mob = loot;
                            }
                        });
                    }
                    // HEADER
                    $logHeader.append($('<div/>').text(mob + ' x ' + mobinfo.count));
                    let icon = !lib.user.isFighting() ? lib.game.getLocationIcon(lib.game.getLocationID(mob)) : lib.game.getMobIcon(mob);
                    icon = lib.user.isStatus('smithing') ? '/images/smithing/'+ mob.toLowerCase().split(' ').join('_')+'.png' : icon;
                    $logHeader.append($('<img/>').addClass('drawer-item-icon').attr('src', icon));
                    $('.drawer-setting-large.clone').before($logEntry);
                    addLogEntry(mob, mobinfo.loot);
                } else {
                    $logEntry = $('.log-entry-'+mob.toLowerCase().split(' ').join('_'));
                    $logHeader = $logEntry.find('.ltf-log-header');
                    $logHeader.find('div').text(mob + ' x ' + mobinfo.count);
                    if (lib.user.isStatus('smithing')) {
                        mob = mobinfo.loot[0];
                    }
                    addLogEntry(mob, mobinfo.loot);
                }
                $logEntry.sort(function (a, b) {
                    return $(a).find('.ltf-log-header').text() - $(b).find('.ltf-log-header').text();
                }).each(function (_, container) {
                    $(container).parent().find('.drawer-setting-large.clone').before(container);
                });
            });
        }
        function addLogEntry(entry, info) {
            if (!entry) { return false; }
            let $logEntry = $('.log-entry-'+entry.toLowerCase().split(' ').join('_'));
            $.each(lib.general.sortObject(info), function(loot, lootinfo) {
                let lootEntryClass = 'loot-entry-'+entry.toLowerCase().split(' ').join('_')+'-'+loot.toLowerCase().split(' ').join('_');
                let $lootEntry = $('.'+lootEntryClass);
                let lootText = loot + ' x ' + lootinfo.count;
                lootText += (lootinfo.procs >= 1) ? ' (+'+lootinfo.procs+')' : '';
                if(!$lootEntry.length) {
                    $lootEntry = $('<div/>', {
                        'class': 'ltf-loot-entry '+lootEntryClass
                    }).text(lootText).appendTo($logEntry.find('.ltf-loot-wrap'));
                    if(lib.game.isRareItem(loot)) {
                        $lootEntry.addClass('loot-rare');
                    } else if(lib.game.isEventItem(loot)) {
                        $lootEntry.addClass('loot-event');
                    } else if(lib.game.isUniversalItem(loot)) {
                        $lootEntry.addClass('loot-universal');
                    }
                } else {
                    $lootEntry.text(lootText);
                }
                $lootEntry.sort(function (a, b) {
                    return $(a).text() - $(b).text();
                }).each(function (_, container) {
                    $(container).parent().append(container);
                });
            });
        }

        /* ############## ACTIONS ############## */
        function getLogs() {
            let logs = [];
            let $logcats = $('.item-log-window:not(.item-log-clone)').find('.item-log-cateogry');
            if ($logcats.length) {
                $logcats.each(function(k,v) {
                    if( $(this).find('.item-log-category-closed').length ) {
                        $(this).find('.item-log-category-closed').click();
                    }
                    if( $(this).find('.item-log-category-open').length ) {
                        logs.push($(this).find('.item-log-category-open').text());
                        $(this).find('.item-log-item').each(function() {
                            if( $(this).text() !== "None" ) {
                                logs.push($(this).text());
                            }
                        });
                    }
                });
            }
            return logs;
        }
        function clearLogs(m = 'all') {
            if (m == 'single') {
                app.runs.unshift({});
            } else if (m == 'all') {
                if (lib.user.exp && lib.user.mexp) {
                    $.each(lib.app.data.active.skills, function(k, skill) {
                        lib.user.exp[skill].init = lib.user.exp[skill].current;
                        lib.user.mexp[skill].init = lib.user.mexp[skill].current;
                    });
                }
                if (Object.keys(lib['lootify'].timers).length !== 0) {
                    $.each(lib['lootify'].timers, function(k, t) {
                        t.reset();
                    });
                }
                app.runs = [{}];
                total = {};
                app.disabled = false;
                app.setup = false;
                $('.item-log-clone').remove();
            }
            $('.item-log-window').find('.drawer-setting-large.active:not(.clone)').click();
        }
        function reset(m = 'all') {
            submitStats(getLogs(), lib.user.getEnchantment('32'));
            clearLogs(m);
        }
        /* ############## GAME LIBRARY ############## */
        function is() {
            return lib.general.getStorage('Terms') === 'true';
        }
        function isLogged(type, txt) {
            let includes = false;
            if(getLogMobs()) {
                $.each(getLogMobs(), function(key, mob) {
                    if (type === 'combat') {
                        if (lib.game.getZoneMobs(txt).includes(mob)) {
                            includes = true;
                        }
                    } else if (type === 'skill') {
                        if (txt == mob) {
                            includes = true;
                        }
                    }
                });
            }
            return includes;
        }
        function isLogEmpty() {
            return Object.keys(app.runs[0]).length === 0;
        }
        function getLogMobs() {
            let mobs = [];
            $.each(total, function(mob, info) {
                mobs.push(mob);
            });
            return mobs;
        }
        function getPasteInterval() {
            let interval = parseInt($('.AutoPasterInterval').val());
            interval = interval ? interval : 30;
            interval = interval < 15 ? 15 : interval;
            interval = interval > 60 ? 60 : interval;
            return interval;
        }
        /* ############## API INTERACTION ############## */
        function setUserPublic(s) {
            lib.app.getRequest(app.name, 'https://digimol.net/idlescape/assets/api.php?a=userpublic&id='+lib.user.id+'&state='+s);
        }
        function canSubmit(logs) {
            let can = true;
            if (lib.user.getEnchantment('7') > 0) {
                can = false;
            }
            if ((lib.user.isFighting() && logs.length < 1) || Object.keys(app.runs[0]).length === 0 || app.disabled) {
                can =  false;
            }
            return can;
        }
        function submitStats(logs, th) {
            if (!canSubmit(logs)) {
                return false;
            }
            lib.user.updateStatus();
            let targetUrl = 'https://docs.google.com/forms/u/0/d/e/1FAIpQLSch3eG9Tqts0tIvnkk-C5JZeTwfbkWXhxkIpFnxyyaNO26h4Q/formResponse';
            let logEntryId = 'entry.558332813';
            let thEntryId = 'entry.22586929';
            let noteEntryId = 'entry.1726819066';
            let finalTH = (parseInt(th) > 0) ? 'TH '+th : 'None';
            let fullUrl = targetUrl + '?'+ noteEntryId + '=Lootify-' + app.ver + '&' + thEntryId + '=' + encodeURIComponent(finalTH) + '&' + logEntryId + '=' + encodeURIComponent(logs.join('\n'));
            if (!lib.user.isFighting()) {
                th = 0;
            } else {
                $.get(fullUrl);
            }
            lib.app.getRequest(app.name, 'https://digimol.net/idlescape/assets/api.php?a=paste&th='+th+'&user='+lib.user.id+'&log='+encodeURIComponent(JSON.stringify(app.runs[0])));
            lib.game.chat({
                'msg': 'Lootify - Loot log pasted!',
                'color': '#00a0fd',
            });
        }
        app.getKills = function() {
            if (!lib.user.id || lib.user.id == 0) { return false; }
            lib.app.getRequest(app.name, 'https://digimol.net/idlescape/assets/api.php?a=getkills&id='+lib.user.id, function(d) {
                lib.user.kills = d;
            });
        }

        /* ############## SOCKET RESPONSES ############## */
        app.initExp = function(d) {
            let e = setInterval(()=> {
                if (app.ready) {
                    clearInterval(e);
                    $.each(lib.app.data.active.skills, function(k, skill) {
                        !lib.user.exp ? lib.user.exp = {} : 1;
                        !lib.user.mexp ? lib.user.mexp = {} : 1;
                        !lib.user.exp[skill] ? lib.user.exp[skill] = {} : 1;
                        lib.user.exp[skill].init = parseInt(d.value.skills[skill].experience);
                        lib.user.exp[skill].current = parseInt(d.value.skills[skill].experience);
                        !lib.user.mexp[skill] ? lib.user.mexp[skill] = {} : 1;
                        lib.user.mexp[skill].init = parseInt(d.value.skills[skill].masteryExperience);
                        lib.user.mexp[skill].current = parseInt(d.value.skills[skill].masteryExperience);
                    });
                }
            });
        }

        app.updateExp = function(msg) {
            let d = JSON.parse(msg)[1];
            if (app.ready) {
                $.each(lib.app.data.active.skills, function(k, skill) {
                    !lib.user.exp ? lib.user.exp = {} : 1;
                    !lib.user.exp[skill] ? lib.user.exp[skill] = {} : 1;
                    !lib.user.mexp ? lib.user.mexp = {} : 1;
                    !lib.user.mexp[skill] ? lib.user.mexp[skill] = {} : 1;
                    if (~msg.indexOf('"'+skill+'"')) {
                        lib.user.exp[skill].current = parseInt(d.value.experience);
                        lib.user.mexp[skill].current = parseInt(d.value.masteryExperience);
                    }
                });
            }
        }

        app.addLogMob = function(d) {
            if (lib.user.lastAction === 'craft') {
                lib.user.lastAction = '';
                return false;
            }
            if (lib.game.isAllowedItem(lib.user.getStatus(), d.item.name)) {
                let loc = lib.game.getLocationName(lib.user.location);
                let item = lib.user.getStockpile(d.item.name);
                let diff = item ? d.item.stackSize - item.stackSize : d.item.stackSize;
                if (diff >= 3 && d.item.name !== 'Gold') { return false; }
                !app.runs[0] ? app.runs[0] = {} : 1;
                !app.runs[0][loc] ? app.runs[0][loc] = {} : 1;
                !app.runs[0][loc].count ? app.runs[0][loc].count = 0 : 1;
                app.runs[0][loc].count++;
                !app.runs[0][loc].loot ? app.runs[0][loc].loot = {} : 1;
                !app.runs[0][loc].loot[d.item.name] ? app.runs[0][loc].loot[d.item.name] = {} : 1;
                !app.runs[0][loc].loot[d.item.name].count ? app.runs[0][loc].loot[d.item.name].count = 0 : 1;
                !app.runs[0][loc].loot[d.item.name].procs ? app.runs[0][loc].loot[d.item.name].procs = 0 : 1;
                if(d.item.name == 'Gold') {
                    app.runs[0][loc].loot[d.item.name].count += diff;
                } else {
                    app.runs[0][loc].loot[d.item.name].count++;
                    diff >= 2 ? app.runs[0][loc].loot[d.item.name].procs++ : 1;
                }
            }
        }

        app.disablePaster = function() {
            if (!app.disabled) {
                app.disabled = true;
                $('.drawer-setting-large.clone').text('Reset Log');
                lib.game.chat({
                    'msg': "Lootify - Paster has been disabled to prevent skewed statistics. You can still use Lootify, but won't be able to paste your data to our server. Please reset your log to enable the paster again!",
                    'color': 'red',
                    'date': false
                });
            }
        }

        lib.app.setCommand('killcount', function() {
            app.getKills();
            let e = setInterval(()=> {
                if(lib.user.kills) {
                    clearInterval(e);
                    let msg = '';
                    let $html = $('<div>');
                    let $wrap = $('<div>',{
                        'class': 'ltf-killcount-wrap'
                    }).appendTo($html);
                    $.each(lib.user.kills, function(mob, count) {
                        let $killcount = $('<div>', {
                            'class': 'ltf-killcount'
                        }).appendTo($wrap);
                        let $icon = $('<img>', {
                            'class': 'dialog-icon ltf-killcount-icon'
                        }).attr({
                            'src': lib.game.getMobIcon(mob)
                        }).appendTo($killcount);
                        let $text = $('<span>', {
                            'class': 'dialog-text-medium lft-killcount-text'
                        }).html(mob+': '+count).appendTo($killcount);
                    });
                    $('<div>', {
                        'class': 'dialog-disclaimer'
                    }).text('These are statistics submitted to Lootify only (updates every 15 minutes)').appendTo($html);
                    msg += $html.html();

                    lib.game.dialog({
                        'title': lib.user.name+' Kill Count',
                        'text': msg,
                        'class': 'ltf-killcount',
                        'type': 'close',
                    });
                }
            }, 100);
        });
    }
})();

if (!window.D4IS_Socket) {
    window.D4IS_Socket = true;
    const sockets = [];
    const nativeWebSocket = window.WebSocket;
    window.WebSocket = function(...args){
        const socket = new nativeWebSocket(...args);
        sockets.push(socket);
        return socket;
    };

    let setupSocket = setInterval(()=> {
        let lib = window.D4IS;
        if(sockets.length != 0){
            clearInterval(setupSocket);
            sockets[0].addEventListener('message', (e) => socketMessageHandler(e));
        }
    }, 10);
}

function socketMessageHandler(e) {
    let lib = window.D4IS;
    let getHandler = setInterval(()=> {
        if (lib) {
            clearInterval(getHandler);
            lib.app.messageHandler(e);
        }
    }, 10)
    }

function includeJS(file) {
    var script  = document.createElement('script');
    script.src  = file;
    script.type = 'text/javascript';
    script.defer = false;

    document.getElementsByTagName('head').item(0).appendChild(script);
}

function includeCSS(file) {
    var style  = document.createElement('link');
    style.rel  = 'stylesheet';
    style.href  = file;
    style.type = 'text/css';

    document.getElementsByTagName('head').item(0).appendChild(style);
}

if (typeof window.D4IS_JS == 'undefined') {
    window.D4IS_JS = true;
    includeJS('https://digimol.net/idlescape/assets/js/lib.js');
}

if (typeof window.D4IS_CSS == 'undefined') {
    window.D4IS_CSS = true;
    includeCSS('https://digimol.net/idlescape/assets/css/game.css');
}