IdleScape - Lootify

Addons for the IdleScape Fighting System

目前為 2021-01-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         IdleScape - Lootify
// @namespace    D4IS
// @version      1.3.6
// @description  Addons for the IdleScape Fighting System
// @author       D4M4G3X
// @match        http*://*idlescape.com/*
// @match        https://idlescape.com/*
// @match        https://www.idlescape.com/*
// @grant        none
// @require http://code.jquery.com/jquery-3.4.1.min.js
// @run-at document-start
// ==/UserScript==

(function() {
    'use strict';
    let ver = '1.3.6';
    let runs = [{}];
    let total = {};
    let totalLoot = {};
    let isSetup = false;
    let time = 0;
    let prices = getMarketPrices();
    let data = {};
    let user = {};
    let app = {};
    let status;
    let location;
    let headers = {};
    let activeSkills = ['mining', 'foraging', 'fishing', 'smithing'];
    let timer = {
        'running': false,
        'active': false,
        'status': 'idling',
        'starts': [],
        'stops': [],
        'reset': function() {
            this.active = false;
            this.running = false;
            this.starts = [];
            this.stops = [];
        },
        'go': function() {
            this.active = true;
            this.update();
            this.interval();
        },
        'interval': function() {
            let e = setInterval(()=> {
                if (timer.running) {
                    if($('.time-stats').length) {
                        $('.time-stats').find('span').text('Elapsed: ' + getTime());
                    }
                }
            }, 100);
        },
        'update': function() {
            let e = setInterval(()=> {
                if (timer.active) {
                    if($('.status-bar').length) {
                        if (status === timer.status) {
                            if (!timer.running) {
                                timer.running = true;
                                timer.starts.unshift(new Date());
                            }
                        } else {
                            if(timer.running) {
                                timer.running = false;
                                timer.stops.unshift(new Date());
                            }
                        }
                    }
                }
            }, 500);
        },
    }
    init();
    /* ############## INTERVALS ############## */
    let mainInterval = setInterval(()=> {
        if ($('.status-bar').length) {
            setupSettings();
            checkUpdates();
            if(!isSetup) {
                isSetup = true;
                getData();
                updateUser();
                setupLogs();
            }
            renderStatus();
            saveLogs();
            mergeLogs();
            renderLogs();
            renderTimer();
            renderKPH();
            renderGPH();
            renderXPH();

            addConfirmations();
            cleanChat();
        }
    }, 1000);
    let pasteInterval = setInterval(()=> {
        if(window.localStorage.getItem(getUsername() + '-AutoPaster') == 'true') {
            reset('single');
        }
    }, getPasteInterval() * 60 * 1000);
    let adInterval = setInterval(()=> {
        chat({
            'msg': 'Lootify - Please report bugs to D4M4G3X#6263 on Discord!',
            'color': '#00a0fd',
        });
    }, 20 * 60 * 1000);
    let updateInterval = setInterval(()=> {
        prices = getMarketPrices();
        getNewVer();
        getAppEnabled();
        getData();
        updateUser();
    }, 15 * 60 * 1000);
    let quickInterval = setInterval(()=> {
        updateStatus();
        addResourceButtons();
        checkRemovals();
        addUserIcons();
    }, 100);
    /* ############## SETUP AND RENDER ############## */
    function init() {
        getNewVer();
        getAppEnabled();
        $('head').append('<link rel="stylesheet" href="https://digimol.net/lootify/css/game.css" type="text/css" />');
    }
    function addUserIcons() {
        $('.message-username').each(function() {
            if(data.testers) {
                if(data.testers.includes($(this).text())) {
                    if (!$(this).find('ltf-user-icon').length) {
                        $(this).prepend($('<img>', {
                            'class': 'message-username-icon ltf-user-icon'
                        }).attr({
                            'src': 'https://digimol.net/lootify/img/lootify_logo.png'
                        }));
                    }
                }
            }
        });
    }
    function addConfirmations() {
        if ($('.combat-zone-clone').length) {
            $('.combat-zone-clone').unbind().click(function(e) {
                let $parent = $(this).parents('.combat-zones');
                let $that = $(this);
                let includes = false;
                if(getLogMobs()) {
                    $.each(getLogMobs(), function(key, mob) {
                        if (getZoneMobs($that.text()).includes(mob)) {
                            includes = true;
                            return false;
                        }
                    });
                    if(isStatus('idling') && Object.keys(runs[0]).length === 0) {
                        $parent.find('.combat-zone').each(function() {
                            if($(this).text() === $that.text()) {
                                reset();
                                $(this).click();
                            }
                        });
                        return false;
                    } else if (!includes) {
                        confirmDialog({
                            '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();
                                    }
                                });
                            },
                        });
                    } else {
                        $parent.find('.combat-zone').each(function() {
                            if($(this).text() === $that.text()) {
                                $(this).click();
                            }
                        });
                    }
                }
            });
        }
        if ($('.resource-button-overlay').length) {
            $('.resource-button-overlay').unbind().click(function(e) {
                let $parent = $(this).parents('.resource-container');
                let includes = false;
                if(getLogMobs()) {
                    $.each(getLogMobs(), function(key, mob) {
                        if ($parent.find('.resource-container-title').text() == mob) {
                            includes = true;
                            return false;
                        }
                    });
                }
                if((isStatus('idling') && Object.keys(runs[0]).length === 0) || isStatus('smithing')) {
                    reset();
                    $parent.find('.resource-button:not(.resource-button-overlay)').click();
                    return false;
                } else if(isFighting() || !includes) {
                    confirmDialog({
                        '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();
                        }
                    });
                } else {
                    $parent.find('.resource-button:not(.resource-button-overlay)').click();
                }
            });
        }
    }
    function addResourceButtons() {
        $.each(activeSkills, function(k, skill) {
            $('.resource-container-button').each(function() {
                if($(this).parents('.theme-'+skill).length) {
                    if( !$(this).next('.resource-button-overlay').length && !~$(this).text().indexOf('Stop')) {
                        let $btn = $(this).find('.resource-button').hide();
                        $('<div>', {
                            'class': 'resource-button-overlay resource-button'
                        }).text($btn.text()).insertAfter($(this));
                    } else {
                        if (~$(this).text().indexOf('Stop')) {
                            $(this).next('.resource-button-overlay').remove();
                            $(this).find('.resource-button').show();
                        }
                    }
                }
            });
        });
        if ($('.combat-zones').length) {
            if(!$('.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);
                });
                $(window).resize(function() {
                    $overlay.width($('.combat-zones').width());
                }).resize();
            }
        }
    }
    function checkRemovals() {
        if ($('.recipe-item').length) {
            $('.recipe-item').each(function() {
                if($(this).find('img').attr('src') === '/images/foraging/log.png') {
                    $(this).remove();
                }
            });
        }
    }
    function setupSettings() {
        let $lootBtn;
        if(!getMenuItem('Lootify', 'category')) {
            let $ltfHeader = setMenuItem({
                'text': 'Lootify',
                'clone': 'Gathering',
                'class': 'hdr-lootify',
                'before': getMenuItem('Gathering', 'category'),
            }, 'category');
            $ltfHeader.append($('<i/>').text(' (v'+ver+')').insertAfter($ltfHeader));
        }
        if (!app.disabled) {
            $('.lootify-error.error-paster').remove();
        }
        if (!$('.lootify-error.error-paster').length && app.disabled) { // TODO: Create Error Message Function
            $('<div/>', {
                'class': 'lootify-error error-paster'
            }).css({
                'color': 'red',
                'width': '80%',
                'margin': '0 auto',
            }).html('Paster is disabled!<br>Reset log to Enable again.').insertBefore(getMenuItem('Lootify Settings'));
        }
        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!').insertBefore(getMenuItem('Lootify Settings'));
        }
        if(!getMenuItem('Lootify')) {
            $lootBtn = setMenuItem({
                'icon': 'https://digimol.net/lootify/img/lootify_logo.png',
                'text': 'Lootify Settings',
                'class': 'btn-lootify',
                'after': getMenuItem('Lootify', 'category'),
                'click': function() {
                    $('.log-paster-settings').toggle();
                },
            });
        } else {
            $lootBtn = $('.btn-lootify');
        }
        if (!$('.log-paster-settings').length) {
            let $wrap = $('<div/>', {
                'class': 'log-paster-settings'
            }).hide();
            $lootBtn.after($wrap);

            addSetting({
                'text': 'Allow Lootify to track your username for your personal experience',
                'name': 'TrackUser132',
                'type': 'checkbox',
                'default': 'false',
                'change': function() {
                    window.localStorage.setItem(getUsername() + '-TrackUser132', $(this).prop('checked'));
                }
            }).appendTo($wrap);
            if (!window.localStorage.getItem(getUsername() + '-TrackUser132')) {
                confirmDialog({
                    'title': 'Requesting permission',
                    'text': 'Please notice that Lootify tracks user data such as username, mob kill statistics/loot data. The purpose of this data is purely for your personal experience. This data can later be used to lookup your personal Lootify statistics and highscores. Do you agree?',
                    'img': true,
                    'cbyes': function() {
                        $('.setting-checkbox.TrackUser132').prop('checked', true);
                        window.localStorage.setItem(getUsername() + '-TrackUser132', 'true');
                    },
                    'cbno': function() {
                        window.localStorage.setItem(getUsername() + '-TrackUser132', 'false');
                    },
                });
            }
            addSetting({
                'text': 'Enable Auto Paster',
                'name': 'AutoPaster',
                'type': 'checkbox',
                'default': 'false',
                'change': function() {
                    window.localStorage.setItem(getUsername() + '-AutoPaster', $(this).prop('checked'));
                }
            }).appendTo($wrap);
            addSetting({
                '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')) {
                        window.localStorage.setItem(getUsername() + '-AutoPasterInterval', $(this).val());
                    }
                }
            }).appendTo($wrap);
            addSetting({
                'text': 'Enable Chat Messages',
                'name': 'ChatMessage',
                'type': 'checkbox',
                'default': 'true',
                'change': function() {
                    window.localStorage.setItem(getUsername() + '-ChatMessage', $(this).prop('checked'));
                }
            }).appendTo($wrap);
        }
    }
    function renderStatus() {
        if (!isFighting() && !isActiveSkill()) {
            $('.ltf-status').remove();
            return false;
        }
        if (!getMenuItem('Status:')) {
            let $kphStats = setMenuItem({
                'class': 'ltf-status',
                'text': 'Status: ' + status,
                'icon': '/images/combat/combat_level.png',
                'after': $('.log-paster-settings'),
                'click': function() {
                    $('[data-for="'+status+'Header"]').click();
                }
            });
        }
        $('.ltf-status').find('span').text('Status: ' + ucfirst(status));
    }
    function setupLogs() {
        if ($('.btn-loot-log').length) {
            $('.btn-loot-log').remove();
        }
        let $logBtn = getMenuItem('Loot Log');
        if (($logBtn && $logBtn.text() === 'Loot Log') && !$logBtn.hasClass('btn-loot-log')) {
            editMenuItem('Loot Log');
        }
        if(!getMenuItem('Loot Log')) {
            setMenuItem({
                'icon': '/images/ui/inventory_icon.png',
                'text': 'Loot Log',
                'class': 'btn-loot-log',
                'before': 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 ltf-submenu').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 Log');
        if (app.disabled) {
            $clone.text('Reset Log');
        }
        getMenuItem('Loot Log').after($clone);
    }
    function renderTimer() {
        if (!isFighting() && !isActiveSkill()) {
            $('.time-stats').remove();
            return false;
        }
        if (!getMenuItem('Elapsed:')) {
            let $timeStats = setMenuItem({
                'class': 'time-stats',
                'text': 'Elapsed: 0S',
                'icon': '/images/clock.png',
                'after': $('.ltf-status')
            });
        }
        setTimeout(()=> {
            if(data.active) {
                if (!timer.active) {
                    timer.go();
                }
                if (data.active.skills.includes(status) || isStatus('fighting')) {
                    timer.status = status;
                }
            }
        }, 1000);
    }
    function renderKPH() {
        if (!isFighting()) {
            $('.kph-stats').remove();
            $('.kph-wrap').remove();
            return false;
        }
        if (!getMenuItem('Kills:')) {
            let $kphStats = 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;
        };
        $('.kph-stats').find('span').text('Kills: ' + addCommas(Math.floor((totalkills()/time)*3600)) + ' p/h');
        $('.kph-wrap').empty();
        $.each(sortObject(mobkills), function(mob, count) {
            let $mobwrap = $('<div/>', {
                'class': 'ltf-header mob-header'
            }).appendTo($('.kph-wrap'));
            $mobwrap.append($('<span/>').css('display','block').text(mob + ': ' + addCommas(Math.floor((count/time)*3600)) + ' p/h'));
        });
    }
    function renderGPH() {
        if((!isActiveSkill() && !isFighting()) || isIron() || isStatus('smithing')) {
            $('.gph-stats').remove();
            $('.gph-wrap').remove();
            return false;
        }
        if (!getMenuItem('Gold:')) {
            let $gphStats = setMenuItem({
                'class': 'gph-stats',
                'text': 'Loot value: 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;
        $.each(totalLoot, function(item, count) {
            if (item === 'Gold') {
                totalGold += count;
            } else {
                totalGold += prices[item] * count;
            }
        });
        $('.gph-stats').find('span').text('Gold: ' + addCommas(Math.floor((totalGold/time)*3600)) + ' p/h');
        $('.gph-wrap').empty();
        let $lootwrap = $('<div/>', {
            'class': 'ltf-header mob-header'
        }).appendTo($('.gph-wrap'));
        $lootwrap.append($('<span/>').css('display','block').text('Loot value: ' + addCommas(Math.floor(totalGold))));
    }
    function renderXPH() {
        if(!isActiveSkill() && !isFighting()) {
            $('.mxph-stats').remove();
            $('.mxph-wrap').remove();
            $('.xph-stats').remove();
            $('.xph-wrap').remove();
            return false;
        }
        let totalExp, totalMexp;

        totalExp = 0;
        if (app.exp && app.mexp) {
            let combatSkills = ['attack', 'defense', 'strength', 'constitution'];
            if (isFighting()) {
                $.each(combatSkills, function(k, skill) {
                    totalExp += app.exp[skill].current - app.exp[skill].init;
                    totalMexp += app.mexp[skill].current - app.mexp[skill].init;
                });
            }
            $.each(activeSkills, function(k, skill) {
                if(isStatus(skill)) {
                    totalExp = app.exp[skill].current - app.exp[skill].init;
                    totalMexp = app.mexp[skill].current - app.mexp[skill].init;
                }
            });
        }
        // EXP COUNTER
        if (!getMenuItem('Experience:') && totalExp > 0) {
            let $xphStats = setMenuItem({
                'class': 'xph-stats',
                'text': 'Experience: 0 p/h',
                'icon': '/images/total_level.png',
                'after': $('.time-stats'),
                'click': function() {
                    $('.xph-wrap').toggle();
                }
            });
            $xphStats.after($('<div/>', {
                'class': 'xph-wrap ltf-submenu'
            }).hide());
        }
        $('.xph-stats').find('span').text('Experience: ' + addCommas(Math.floor((totalExp/time)*3600)) + ' p/h');
        $('.xph-wrap').empty();
        let $expwrap = $('<div/>', {
            'class': 'ltf-header mob-header'
        }).appendTo($('.xph-wrap'));
        $expwrap.append($('<span/>').css('display','block').text('Exp earned: ' + addCommas(Math.floor(totalExp))));

        if (!getMenuItem('Mastery:') && totalMexp > 0) {
            let $mxphStats = setMenuItem({
                'class': 'mxph-stats',
                'text': 'Mastery: 0 p/h',
                'icon': '/images/total_level_mastery_icon.png',
                'after': $('.time-stats'),
                'click': function() {
                    $('.mxph-wrap').toggle();
                }
            });
            $mxphStats.after($('<div/>', {
                'class': 'mxph-wrap ltf-submenu'
            }).hide());
        }
        $('.mxph-stats').find('span').text('Mastery: ' + addCommas(Math.floor((totalMexp/time)*3600)) + ' p/h');
        $('.mxph-wrap').empty();
        let $mexpwrap = $('<div/>', {
            'class': 'ltf-header mob-header'
        }).appendTo($('.mxph-wrap'));
        $mexpwrap.append($('<span/>').css('display','block').text('Mastery earned: ' + addCommas(Math.floor(totalMexp))));
    }
    function saveLogs() {
        if (!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 ");
                !runs[0][mobs[0]] ? runs[0][mobs[0]] = {} : 1;
                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 ");
                        !runs[0][mobs[0]].loot ? runs[0][mobs[0]].loot = {} : 1;
                        !runs[0][mobs[0]].loot[loot[0]] ? runs[0][mobs[0]].loot[loot[0]] = {} : 1;
                        runs[0][mobs[0]].loot[loot[0]].count = parseInt(loot[1]);
                    }
                });
            });
        }
    }
    function mergeLogs() {
        if(runs) {
            total = {};
            totalLoot = {};
            $.each(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].count += lootinfo.count;
                        /* SET TOTAL LOOT OVERALL */
                        !totalLoot[lootname] ? totalLoot[lootname] = 0 : 1;
                        totalLoot[lootname] += lootinfo.count;
                    });
                });
            });
            delete total[''];
        }
    }
    function renderLogs() {
        if(!runs || isStatus('smithing')) { return; }
        let $cat, $mobwrap, $mobheader, $lootentry;
        $('.item-log-clone').find('.item-log-cateogry').remove();
        $.each(sortObject(total), function(mob, mobinfo) {
            $cat = $('<div/>', {
                'class':'item-log-cateogry noselect'
            });
            $mobwrap = $('<div/>', {
                'class': 'ltf-header loot-header'
            }).appendTo($cat);
            $cat.click(function() {
                headers[mob] = headers[mob] == false ? true : false;
                $mobwrap.find('.loot-entry').toggle();
            });
            $mobwrap.append($('<div/>').text(mob + ' x ' + mobinfo.count));
            let icon = (status != 'fighting') ? getLocationIcon(getLocationID(mob)) : getMobIcon(mob);
            $mobwrap.append($('<img/>').addClass('drawer-item-icon').attr('src', icon));
            if(mobinfo.loot) {
                $.each(sortObject(mobinfo.loot), function(loot, lootinfo) {
                    $lootentry = $('<div/>', {
                        'class': 'loot-entry'
                    }).text(loot + ' x ' + lootinfo.count).appendTo($cat);
                    if(isRare(loot)) {
                        $lootentry.addClass('loot-rare');
                    } else if(isEvent(loot)) {
                        $lootentry.addClass('loot-event');
                    } else if(isUniversal(loot)) {
                        $lootentry.addClass('loot-universal');
                    }
                    headers[mob] = headers[mob] ? headers[mob] : true;
                    if (!headers[mob]) {
                        $lootentry.hide();
                    }
                });
            } else {
                $lootentry = $('<div/>', {
                    'class': 'loot-entry'
                }).html('<i>None</i>').appendTo($cat);
                headers[mob] = headers[mob] ? headers[mob] : false;
                if (!headers[mob]) {
                    $lootentry.hide();
                }
            }
            $('.drawer-setting-large.clone').before($cat);
        });
    }
    /* ############## 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') {
            runs.unshift({});
        } else if (m == 'all') {
            if (app.exp && app.mexp) {
                $.each(data.active.skills, function(k, skill) {
                    app.exp[skill].init = app.exp[skill].current;
                    app.mexp[skill].init = app.mexp[skill].current;
                });
            }
            timer.reset();
            runs = [{}];
            total = {};
            app.disabled = false;
            isSetup = false;
            $('.item-log-clone').remove();
        }
        $('.item-log-window').find('.drawer-setting-large.active:not(.clone)').click();
    }
    function reset(m = 'all') {
        submitStats(getLogs(), getTH());
        clearLogs(m);
    }
    /* ############## GAME LIBRARY ############## */
    function isIron() {
        return $('.header-league-icon').attr('src') === '/images/leagues/ironman_league_icon_v5.png';
    }
    function isStatus(txt) {
        return status === txt;
    }
    function isActiveSkill() {
        let isActive = false;
        if (data.active) {
            $.each(data.active.skills, function(k, skill) {
                if(isStatus(skill)) {
                    isActive = true;
                    return false;
                }
            });
        }
        return isActive;
    }
    function isFighting() {
        return isStatus('fighting');
    }
    function isUniversal(txt) {
        return data.item ? data.item.universal.includes(txt) : '';
    }
    function isRare(txt) {
        return data.item ? data.item.rare.includes(txt) : '';
    }
    function isEvent(txt) {
        return data.item ? data.item.event.includes(txt) : '';
    }
    function getZoneMobs(zone) {
        return data.zone ? data.zone[zone] : '';
    }
    function getLocation(loc) {
        return data.location ? data.location.name[loc] : '';
    }
    function getLocationID(loc) {
        let id = 0;
        if (data.location) {
            $.each(data.location.name, function(k,v) {
                if (v === loc) {
                    id = k;
                }
            });
        }
        return id;
    }
    function getLocationIcon(loc) {
        return data.location ? data.location.icon[loc] : '';
    }
    function getMobIcon(name) {
        return data.icon ? data.icon.mob[name] : '';
    }
    function getUsername() {
        return $('.navbar1-box').text().split(' ')[1];
    }
    function getTH() {
        return getEnchantment('32');
    }
    function getEnchantment(id) {
        let TH = 0;
        if(user.enchantments) {
            $.each(user.enchantments, function(k, v) {
                if(v.enchantmentID == id) {
                    TH = v.enchantmentStrength;
                }
            });
        }
        return TH;
    }
    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;
    }
    function getMarketPrices() {
        let p = {};
        $.getJSON( "https://api.idlescape.xyz/prices", function( d ) {
            if(d) {
                $.each(d.items, function(k, v) {
                    p[v.name] = v.price;
                });
            } else {
                p = false;
            }
        });
        return p;
    }
    /* === ACTIONS === */
    function chat(args) {
        if(window.localStorage.getItem(getUsername() + '-ChatMessage') === 'false') {
            return false;
        }
        let e = setInterval(()=> {
            let $chat = $('.chat-message-container > .chat-message-list > div');
            if( $chat.length ) {
                clearInterval(e);
                $chat.each(function() {
                    if (!$(this).find('.activity-log').length) {
                        let $msg = $('<div/>', {
                            'class': 'chat-message msg-lootify'
                        }).clone().appendTo($(this));
                        if (args['date'] !== false) {
                            let $date = $('<span/>', {
                                'class': 'message-time-stamp',
                            }).text('['+getDate(new Date)+']').appendTo($msg);
                        }
                        args['color'] = args['color'] ? args['color'] : '#00A0FD';
                        args['glow'] = args['glow'] ? '0 0 3px '+args['glow'] : 'none';
                        let $txt = $('<span/>', {
                            'class':'chat-message-system'
                        }).css({
                            'font-size': '14px',
                            'color': args['color'],
                            'text-shadow': args['glow'],
                        }).text(args['msg']).appendTo($msg);

                        args['ttl'] = args['ttl'] ? args['ttl'] : 5;
                        setTimeout(()=> {
                            $msg.remove();
                        }, args['ttl'] * 60 * 1000);
                    }
                });
            }
        }, 1000);
    }
    function cleanChat() {
        let $chat = $('.chat-message-container');
        if( $chat.length ) {
            $chat.each(function() {
                if($(this).find('.activity-log').length) {
                    $(this).find('.msg-lootify').remove();
                }
            });
        }
    }
    function checkUpdates() {
        if (app.newver && ver < app.newver) {
            if (!$('.lootify-update').length) {
                if(!~app.newver.toLowerCase().indexOf('error')) {
                    $('<div/>', {
                        'class': 'lootify-update'
                    }).css({
                        'color': 'red',
                        'width': '80%',
                        'margin': '0 auto',
                    }).text('Update: v'+app.newver+' available!').insertBefore(getMenuItem('Lootify Settings'));
                }
            }
        }
    }
    /* ############## API INTERACTION ############## */
    function getAppEnabled() {
        getRequest('https://digimol.net/lootify/const.php?t=enabled', function(d) {
            app.enabled = d ? d.toString() : 0;
        });
    }
    function getNewVer() {
        getRequest('https://digimol.net/lootify/const.php?t=ver&m=current', function(d) {
            app.newver = d ? d.toString() : 0;
        });
    }
    function getData() {
        getRequest('https://digimol.net/lootify/json.php?d=data', function(d) {
            data = d ? JSON.parse(d) : {};
            data.active.skills.push('smithing');
        });
    }
    function updateUser() {
        getRequest('https://digimol.net/lootify/api.php?a=updateuser&id='+user.id+'&name='+user.name);
    }
    function getRequest(url, cb = function(){}) {
        $.get('https://digimol.net/lootify/api.php?a=getauth').done(function(d1) {
            $.get(url+'&auth='+d1+'&ver='+ver).done(function(d2) {
                cb(d2);
            });
        });
    }
    function submitStats(logs, th) {
        if ((isFighting() && logs.length < 1) || Object.keys(runs[0]).length === 0 || app.disabled || (!app.enabled || app.enabled === 'false')) {
            return false;
        }
        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-' + ver + '&' + thEntryId + '=' + encodeURIComponent(finalTH) + '&' + logEntryId + '=' + encodeURIComponent(logs.join('\n'));
        if (!isFighting()) {
            th = 0;
        } else {
            $.get(fullUrl);
        }

        getRequest('https://digimol.net/lootify/api.php?a=paste&th='+th+'&user='+user.id+'&log='+encodeURIComponent(JSON.stringify(runs[0])));
        chat({
            'msg': 'Lootify - Loot log pasted!',
            'color': '#00a0fd',
        });
    }
    /* ############## NAVIGATION ############## */
    function setMenuItem(args, type = 'item') {
        let $item, $img, e;
        args['text'] = args['text'] ? args['text'] : 'Menu item';
        args['clone'] = args['clone'] ? args['clone'] : 'Shops';
        if(!getMenuItem(args['clone'], type)) { return $('<div>'); }
        $item = getMenuItem(args['clone'], type).clone();
        if(args['class']) {
            $item.addClass(args['class']);
        }
        if(args['css']) {
            $item.css(args['css']);
        }
        $img = $item.find('img').clone();
        $item.unbind().empty();
        if(type === 'item') {
            $item.append($('<span/>').text(args['text']));
        } else if (type === 'category') {
            $item.append($('<b/>').text(args['text']));
        }
        if (args['icon']) {
            $img.attr('src', args['icon']).prependTo($item);
        }
        args['before'] ? args['before'].before($item) : 1;
        args['after'] ? args['after'].after($item) : 1;
        if (typeof args['click'] === 'function') {
            $item.click(args['click']);
        }
        return $item;
    }
    function getMenuItem(txt, type='item') {
        let $item;
        $.each($('.drawer-' + type), function() {
            if (~$(this).text().indexOf(txt)) {
                $item = $(this);
            }
        });
        return $item;
    }
    function editMenuItem(txt) {
        getMenuItem(txt).text('Hidden').hide();
    }
    function removeMenuItem(txt) {
        getMenuItem(txt).remove();
    }
    function addSetting(args) {
        args['text'] = args['text'] ? args['text'] : 'New setting';
        args['name'] = args['name'] ? args['name'] : args['text'].replace(' ', '-').toLowerCase();
        args['type'] = args['type'] ? args['type'] : 'text';
        args['default'] = args['default'] ? args['default'] : 0;
        let $setting = $('<div/>', {
            'class': 'ltf-setting'
        }).append($('<span/>')).append($('<input/>').addClass('setting-'+args['type']));
        $setting.find('span').text(args['text']);
        $setting.find('input').addClass(args['name']).attr('type', args['type']);
        if (args['min']) {
            $setting.find('input').attr('min', args['min']);
        }
        if (args['max']) {
            $setting.find('input').attr('max', args['max']);
        }
        let val = window.localStorage.getItem(getUsername() + '-' + args['name']);
        if (args['type'] === 'checkbox') {
            val = val ? val : args['default'];
            val = val == 'true' ? true : false;
            $setting.find('input').prop('checked', val);
        }
        if (val) {
            $setting.find('input').val(val)
        } else {
            $setting.find('input').val(args['default']);
        }
        if (args['change']) {
            $setting.find('input').change(args['change']);
        }
        return $setting;
    }
    function confirmDialog(args) {
        let $root = $('<div>', {
            'class': 'MuiDialog-root'
        }).attr({
            'role': 'presentation'
        }).css({
            'position': 'fixed',
            'z-index': '1300',
            'inset': '0px'
        }).appendTo($('body'));

        let $backdrop = $('<div>', {
            'class': 'MuiBackdrop-root'
        }).attr({
            'aria-hidden': 'true'
        }).css({
            'opacity': '1',
            'transition': 'opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
            'z-index': '100'
        }).appendTo($root);

        let $container = $('<div>', {
            'class': 'MuiDialog-container MuiDialog-scrollPaper',
        }).attr({
            'role': 'none presentation'
        }).css({
            'opacity': '1',
            'transition': 'opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms'
        }).appendTo($root);

        let $paper = $('<div>', {
            'class': 'MuiPaper-root MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthSm MuiPaper-elevation24 MuiPaper-rounded'
        }).attr({
            'role': 'dialog'
        }).css({
            'z-index': '200'
        }).appendTo($container);

        if (args['img']) {
            let $image = $('<img>', {
                'class': 'ltf-dialog-image'
            }).attr({
                'src': 'https://digimol.net/lootify/img/lootify_logo.png'
            }).appendTo($paper);
        }

        let $title = $('<div>', {
            'class': 'MuiDialogTitle-root'
        }).appendTo($paper);

        $('<h2>', {
            'class': 'MuiTypography-root MuiTypography-h6'
        }).text(args['title']).appendTo($title);

        $('<p>', {
            'class': 'MuiTypography-root MuiDialogContentText-root MuiTypography-body1 MuiTypography-colorTextSecondary'
        }).text(args['text']).appendTo($paper);

        let $actions = $('<div>', {
            'class':  'MuiDialogActions-root MuiDialogActions-spacing'
        }).appendTo($paper);

        if(!args['cbyes']) {
            args['cbyes'] = function(){};
        }

        if(!args['cbno']) {
            args['cbno'] = function(){};
        }

        $backdrop.click(function() {
            $root.remove();
        });

        let $button = [];
        $button[0] = $('<div>', {
            'class': 'item-dialogue-button idlescape-button idlescape-button-green'
        }).attr({
            'variant': 'contained',
            'color': 'secondary'
        }).text('Yes').appendTo($actions);

        $button[0].click(function() {
            $root.remove();
            args['cbyes']();
        });

        $button[1] = $('<div>', {
            'class': 'item-dialogue-button idlescape-button idlescape-button-red'
        }).attr({
            'variant': 'contained',
            'color': 'secondary'
        }).text('No').appendTo($actions);

        $button[1].click(function() {
            $root.remove();
            args['cbno']();
        });
    }
    /* ############## SOCKET SETUP ############## */
    const sockets = [];
    const nativeWebSocket = window.WebSocket;
    window.WebSocket = function(...args){
        const socket = new nativeWebSocket(...args);
        sockets.push(socket);
        return socket;
    };
    let setupSocket = setInterval(()=> {
        if(sockets.length != 0){
            clearInterval(setupSocket);
            sockets[0].addEventListener('message', (e) => messageHandler(e));
        }
    }, 1000);
    function messageHandler(e) {
        let msg = e.data;
        msg = (msg.match(/^[0-9]+(\[.+)$/) || [])[1];
        if(msg && ~msg.indexOf('"update player"')) {
            let d = JSON.parse(msg.split('Socket'))[1];
            if(~msg.indexOf('activeEnchantments')) {
                if(~msg.indexOf('"portion":"all"')) {
                    user.enchantments = d.value.activeEnchantments;
                    if(window.localStorage.getItem(getUsername() + '-TrackUser132') === 'true') {
                        user.name = d.value.username;
                    } else {
                        user.name = 'Unknown';
                    }
                    user.id = d.value.id;
                    let e = setInterval(()=> {
                        if (data.active) {
                            clearInterval(e);
                            $.each(data.active.skills, function(k, skill) {
                                !app.exp ? app.exp = {} : 1;
                                !app.mexp ? app.mexp = {} : 1;
                                !app.exp[skill] ? app.exp[skill] = {} : 1;
                                app.exp[skill].init = parseInt(d.value.skills[skill].experience);
                                app.exp[skill].current = parseInt(d.value.skills[skill].experience);
                                !app.mexp[skill] ? app.mexp[skill] = {} : 1;
                                app.mexp[skill].init = parseInt(d.value.skills[skill].masteryExperience);
                                app.mexp[skill].current = parseInt(d.value.skills[skill].masteryExperience);
                            });
                        }
                    });
                } else {
                    user.enchantments = d.value[0];
                }

            }
        }
        // GET MINING AND FORAGING ITEMS
        if (isActiveSkill()) {
            if (msg && ~msg.indexOf('"update inventory"') && !~msg.indexOf('Essence')) {
                let d = JSON.parse(msg)[1];
                if( data.skill && data.skill[status].allowed.includes(d.item.name) ) {
                    let loc = getLocation(location);
                    !runs[0][loc] ? runs[0][loc] = {} : 1;
                    !runs[0][loc].count ? runs[0][loc].count = 0 : 1;
                    runs[0][loc].count++;
                    !runs[0][loc].loot ? runs[0][loc].loot = {} : 1;
                    !runs[0][loc].loot[d.item.name] ? runs[0][loc].loot[d.item.name] = {} : 1;
                    !runs[0][loc].loot[d.item.name].count ? runs[0][loc].loot[d.item.name].count = 0 : 1;
                    runs[0][loc].loot[d.item.name].count++;
                }
            }
        }
        // GET COMBAT EXP
        if(msg && ~msg.indexOf('"update player"') && ~msg.indexOf('"skills"')) {
            let d = JSON.parse(msg)[1];
            if (data.active) {
                $.each(data.active.skills, function(k, skill) {
                    !app.exp ? app.exp = {} : 1;
                    !app.exp[skill] ? app.exp[skill] = {} : 1;
                    !app.mexp ? app.mexp = {} : 1;
                    !app.mexp[skill] ? app.mexp[skill] = {} : 1;
                    if (~msg.indexOf('"'+skill+'"')) {
                        app.exp[skill].current = parseInt(d.value.experience);
                        app.mexp[skill].current = parseInt(d.value.masteryExperience);
                    }
                });
            }
        }
        // UPDATE STATUS
        if(msg && ~msg.indexOf('"start animation"')) {
            let d = JSON.parse(msg)[1];
            status = d.action;
            location = d.location;
        }
        // CHECK IF PLAYER WENT TO MARKET
        if(msg && ~msg.indexOf('"get player marketplace items"')) {
            let d = JSON.parse(msg)[1];
            if (!app.disabled) {
                app.disabled = true;
                $('.drawer-setting-large.clone').text('Reset Log');
                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
                });
            }
        }
        if(msg && ~msg.indexOf('"update inventory"') && ~msg.indexOf('vault')) {
            let d = JSON.parse(msg)[1];
            if (!app.disabled) {
                app.disabled = true;
                $('.drawer-setting-large.clone').text('Reset Log');
                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
                });
            }
        }
    }
    function updateStatus() {
        if ($('.status-bar').length) {
            if(~$('.status-bar').text().indexOf('Fighting')) {
                status = 'fighting';
            } else if(~$('.status-bar').text().indexOf('Idling')) {
                status = 'idling';
            }
        }
    }
    /* ############## GENERAL LIBRARY ############## */
    function addCommas(nStr) {
        nStr += '';
        let x = nStr.split('.');
        let x1 = x[0];
        let x2 = x.length > 1 ? '.' + x[1] : '';
        let rgx = /(\d+)(\d{3})/;
        while (rgx.test(x1)) {
            x1 = x1.replace(rgx, '$1' + ',' + '$2');
        }
        return x1 + x2;
    }
    function getDate(date) {
        let h = date.getHours();
        let m = date.getMinutes();
        let s = date.getSeconds()
        m = m < 10 ? '0'+m : m;
        s = s < 10 ? '0'+s : s;
        h = h < 10 ? '0'+h : h;
        let strTime = h + ':' + m + ':' + s;
        if(~$('.chat-message .message-time-stamp').text().indexOf(' AM]') || ~$('.chat-message .message-time-stamp').text().indexOf(' PM]')) {
            let ampm = parseInt(h) >= 12 ? 'PM' : 'AM';
            h = h % 12;
            h = h ? h : 12;
            strTime = h + ':' + m + ':' + s + ' '  + ampm
        }
        return strTime;
    }
    function getTime() {
        let count = 0;
        $.each(timer.starts, function(k, start) {
            let stop = !timer.stops[k] ? new Date() : timer.stops[k];
            count += +stop - +start;
        });
        let s = Math.floor((count /  1000)) % 60;
        let m = Math.floor((count / 60000)) % 60;
        let h = Math.floor((count / 3600000)) % 24;
        let d = Math.floor((count / 86400000)) % 7;
        let w = Math.floor((count / 604800000)) % 52;
        let y = Math.floor((count / 31557600000));
        let timeStr = '';
        timeStr += (y>0) ? y+'Y ' : '';
        timeStr += (w>0) ? w+'W ' : '';
        timeStr += (d>0) ? d+'D ' : '';
        timeStr += (h>0) ? h+'H ' : '';
        timeStr += (m>0) ? m+'M ' : '';
        timeStr += (s>0) ? s+'S ' : '';
        time = (count /  1000);
        return timeStr;
    }
    function sortObject(o) {
        var sorted = {},
            key, a = [];
        for (key in o) {
            if (o.hasOwnProperty(key)) {
                a.push(key);
            }
        }
        a.sort();
        for (key = 0; key < a.length; key++) {
            sorted[a[key]] = o[a[key]];
        }
        return sorted;
    }
    function ucfirst(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

})();