smmo+

simple-mmo.com improvements

Verze ze dne 26. 09. 2019. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         smmo+
// @namespace    https://simple-mmo.com/
// @version      0.0.21
// @description  simple-mmo.com improvements
// @author       somebody
// @match        https://simple-mmo.com/*
// @match        http://simple-mmo.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

/* globals $, closeNav, openNav, inventoryFilter, marketFilter */
(function() {
    'use strict';

    function xhr(method, url, cb) {
        var req = new XMLHttpRequest();
        req.addEventListener('load', function () {
            let response = this.response;
            try {
                response = JSON.parse(response);
            } catch (e) {
            }
            cb(response);
        });
        req.open(method, url);
        req.send();
    }

    const IS_DARK_THEME = getComputedStyle(document.body).backgroundColor === 'rgb(51, 51, 51)';
    const JOB_GOLD_LOOKUP = {
        'Novice Blacksmith': 10,
        'Apprentice Blacksmith': 100,
        'Adept Blacksmith': 500,
        'Master Blacksmith': 1000,
        'Legendary Blacksmith': 2500,
        'Petty Crook': 30,
        'Pickpocket': 250,
        'Skilled Burglar': 650,
        'Master Thief': 1500,
        'Legendary Thief': 2750,
        'Couch Potato': 10,
        'Sandwich Artist': 100,
        'Finally a Chef': 500,
        'Master Chef': 1000,
        'Lord of the Fries': 2500,
        'Novice Guard': 10,
        'Apprentice Guard': 100,
        'Adept Guard': 500,
        'Master Guard': 1000,
        'Legendary Guard': 2500,
        'Novice Banker': 50,
        'Apprentice Banker': 325,
        'Adept Banker': 850,
        'Master Banker': 2300,
        'Legendary Banker': 3800,
    };

    let emojiHandle = setInterval(function () {
        if (!unsafeWindow.$) {
            return;
        }
        clearInterval(emojiHandle);
        unsafeWindow.jQuery = unsafeWindow.$;
        let emojioneAreaCss = document.createElement('link');
        emojioneAreaCss.rel = 'stylesheet';
        emojioneAreaCss.href = 'https://cdn.jsdelivr.net/gh/mervick/[email protected]/dist/emojionearea.min.css';
        document.head.appendChild(emojioneAreaCss);
        let emojioneAreaJs = document.createElement('script');
        emojioneAreaJs.src = 'https://cdn.jsdelivr.net/gh/mervick/[email protected]/dist/emojionearea.min.js';
        document.body.appendChild(emojioneAreaJs);
        let emojiHandle2 = setInterval(function () {
            unsafeWindow.$('#chatText').emojioneArea({pickerPosition: 'bottom'});
            document.getElementById('chattextBtn').addEventListener('click', function () {
                setTimeout(function () {
                    document.getElementsByClassName('emojionearea-editor')[0].innerHTML = '';
                }, 100);
            });
            clearInterval(emojiHandle2);
        }, 100);
    }, 100);

    let sidenav = document.getElementById('mySidenav');
    document.getElementsByClassName('container-two')[0].appendChild(sidenav);

    function darkenedCss (clazz) {
        return `
.${clazz}::before {
    z-index: -1;
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: ${document.getElementsByClassName(clazz)[0] ? getComputedStyle(document.getElementsByClassName(clazz)[0]).background : 'none'};
    filter: brightness(50%);
}

.${clazz} {
    background: none;
}
`;
    }

    if (IS_DARK_THEME) {
        document.querySelectorAll('span[style*="rgba(0,0,0,"]').forEach(function (e) { e.style.color = e.style.color.replace('(0, 0, 0, ', '(255, 255, 255, '); });
        document.querySelectorAll('img[src="/img/online.gif"]').forEach(function (e) { e.style.filter = 'invert(1)'; });
        let style = document.createElement('style');
        style.textContent = `\
${darkenedCss('travel')}
${darkenedCss('attackbg')}
${darkenedCss('surround')}

.container {
    width: 100%;
    overflow-y: scroll;
}

.container-two {
    display: flex;
    flex-flow: column nowrap;
}

#mySidenav {
    position: unset;
    flex: 0 0 0;
    width: 100% !important;
}

#chatload {
    width: 100%;
}

#chatArea tr {
    height: 0;
}

#chatArea th {
    white-space: nowrap !important;
    padding: 0 6px !important;
    position: unset !important;
    height: 0;
}

#chatArea th:nth-child(3) {
    white-space: normal !important;
}

.progress-moved .progress-bar2 {
    background-color: #8d0c2a;
}

.demo-card-wide > .mdl-card__title {
    filter: brightness(50%);
}

.speech-bubble-you {
    color: #FFF;
}

.speech-bubble-them {
    color: #FFF;
    background: #121212;
}

.speech-bubble-them:after {
    border-right-color: #121212;
}

.speech-bubble {
    color: rgba(255, 255, 255, 0.6);
    background: #0f0f0f;
}

.speech-bubble:after {
    border-top-color: #0f0f0f;
}

.cta {
    color: rgb(255, 255, 255, 0.6);
    background-color: rgb(17, 17, 17);
}

.cta:hover, .cta:focus {
    color: rgb(255, 255, 255, 0.9);
}

h1, h2, h3, h4, h5, h6 {
    color: white;
}

#stepUsercountContainer > div {
    background: #0a0a0a !important;
    color: white !important;
}

.mdl-textfield__input::placeholder {
    color: rgba(255,255,255,0.4);
}

.mdl-textfield__input {
    color: rgba(255,255,255,0.7) !important;
}

.fab {
    color: rgba(255, 255, 255, 0.6);
    background: rgb(0, 0, 0);
}

.emojionearea .emojionearea-editor {
    min-height: 5vh;
}`;
        document.body.appendChild(style);
    }

    // sidebar
    let sidebarContainer = document.createElement('div'),
        sidebar = document.createElement('div');
    sidebarContainer.appendChild(sidebar);
    sidebar.id = 's-sidebar';
    let computed = getComputedStyle(document.getElementById('mySidenav'));
    sidebarContainer.style.flex = '0 0 0';
    sidebar.style.position = 'fixed';
    sidebar.style.height = '100%';
    sidebar.style.width = '0';
    sidebar.style.display = 'flex';
    sidebar.style.flexFlow = 'column nowrap';
    sidebar.style.overflowY = 'auto';
    sidebar.style.background = IS_DARK_THEME ? '#222' : '#DDD';
    for (let prop of ['transition', 'overflowX']) {
        sidebarContainer.style[prop] = sidebar.style[prop] = computed[prop];
    }

    let sidebarVisible = GM_getValue('sidebarVisible', false);

    function openSidebar(event) {
        if (sidebarVisible) {
            return;
        }
        GM_setValue('sidebarVisible', sidebarVisible = true);
        sidebarContainer.style.flex = '0 0 200px';
        sidebar.style.width = '200px';
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }
    }

    function closeSidebar(event) {
        if (!sidebarVisible) {
            return;
        }
        GM_setValue('sidebarVisible', sidebarVisible = false);
        sidebarContainer.style.flex = '0 0 0';
        sidebar.style.width = '0';
    }

    if (sidebarVisible) {
        sidebarVisible = false;
        openSidebar();
    }

    let chatVisible = GM_getValue('chatVisible', false);

    let updateChatHandle;
    let messages = [];

    function closeChat(event) {
        if (!chatVisible) {
            return;
        }
        GM_setValue('chatVisible', chatVisible = false);
        sidenav.style.flex = '0 0 0';
        messages = [];
        clearInterval(updateChatHandle);
        unsafeWindow.closeNav();
    }

    function updateChatPerSecond() {
        let now = +new Date();
        if (messages.every(([_, time]) => now - time >= 60000)) {
            clearInterval(updateChatHandle);
            updateChatHandle = setInterval(function () {
                let now = +new Date();
                for (let [el, time] of messages) {
                    let mins = Math.floor((now - time) / 60000);
                    el.innerText = mins === 1 ? '1 minute ago' : mins + ' minutes ago';
                }
            }, 15000);
        }
        for (let [el, time] of messages) {
            let mins = Math.floor((now - time) / 60000);
            let secs = Math.floor((now - time) / 1000) % 60;
            el.innerText = mins === 0 ? secs === 1 ? '1 second ago' : secs + ' seconds ago' : mins === 1 ? '1 minute ago' : mins + ' minutes ago';
        }
    }

    function openChat(event) {
        if (chatVisible) {
            return;
        }
        GM_setValue('chatVisible', chatVisible = true);
        sidenav.style.flex = '0 0 30%';
        updateChatHandle = setInterval(updateChatPerSecond, 1000);
        unsafeWindow.openNav();
    }
    unsafeWindow.openChat = openChat;

    if (chatVisible) {
        chatVisible = false;
        let chatOpenHandle = setInterval(function () {
            if (!unsafeWindow.openNav || !unsafeWindow.startChatSSE) {
                return;
            }
            openChat();
            clearInterval(chatOpenHandle);
        }, 100);
    }

    // horizontal chat
    let chatbox = document.getElementById('mySidenav');
    chatbox.style.display = 'flex';
    chatbox.style.flexFlow = 'column nowrap';
    document.getElementById('chatContainer').style.overflowY = 'scroll';
    let chatarea = document.getElementById('chatArea');
    let chatloadObserver = new MutationObserver(function () {
        let messageEls = [...chatarea.children].map(child => child.children[1]);
        for (let message of messageEls) {
            if (message.childNodes.length !== 6) {
                // already processed
                continue;
            }
            let date = +new Date();
            let dateText = message.childNodes[3].innerText;
            let dateMatch;
            if (dateMatch = dateText.match(/\d+(?= minutes? ago)/)) {
                date -= dateMatch[0] * 60000;
            } else if (dateMatch = dateText.match(/\d+(?= seconds? ago)/)) {
                date -= dateMatch[0] * 1000;
            }
            messages.push([message.childNodes[3], date]);
            let parent = message.parentNode;
            let th = document.createElement('th');
            th.appendChild(message.childNodes[5]);
            th.style.width = '0';
            th.style.wordBreak = 'break-word';
            parent.appendChild(th);
            th = document.createElement('th');
            th.appendChild(message.childNodes[3]);
            parent.appendChild(th);
            parent.children[0].style.display = 'none';
            message.removeChild(message.childNodes[2]);
            message.removeChild(message.childNodes[1]);
        }
        clearInterval(updateChatHandle);
        updateChatHandle = setInterval(updateChatPerSecond, 1000);
    });
    let closeButton = document.createElement('button');
    closeButton.classList.add('cta');
    closeButton.innerText = 'Close';
    closeButton.addEventListener('click', closeChat);
    document.getElementById('chattextBtn').parentNode.appendChild(closeButton);
    chatloadObserver.observe(chatarea, {childList: true});
    let sidenavObserver = new MutationObserver(function () {
        if (chatVisible && sidenav.style.width === '0px') {
            unsafeWindow.openNav();
        }
    });
    sidenavObserver.observe(sidenav, {attributes: true});

    let playerInfo = document.createElement('div');
    playerInfo.innerHTML = `\
<div style="padding:4px 8px;display:flex;flex-flow:row nowrap;align-items:center">
    <div style="padding:0 8px"><img id="s-avatar" src="/img/sprites/0.png"></div>
    <center style="width:100%">
        <a style="font-weight:bold;color:${IS_DARK_THEME ? 'white' : 'black'}" id="s-name" href="/me"></a><br />
        Level <span id="s-level"></span>
    </center>
    <span style="cursor:pointer;font-size:16pt" id="s-close">×</span>
</div>
<center style="padding:4px 8px">
    Experience:<br />
    <span id="s-xp">0</span>/<span id="s-xpm">50</span>
    <div style="width:120px;height:8px;background:#111"><div id="s-xpb" style="width:0%;float:left;height:100%;background:#aaa"></div></div>
    Health:<br />
    <span id="s-hp">50</span>/<span id="s-hpm">50</span>
    <div style="width:120px;height:8px;background:#111"><div id="s-hpb" style="width:100%;float:left;height:100%;background:#aaa"></div></div>
    Energy:<br />
    <span id="s-ep">5</span>/<span id="s-epm">5</span>
    <div style="width:120px;height:8px;background:#111"><div id="s-epb" style="width:100%;float:left;height:100%;background:#aaa"></div></div>
    Steps:<br />
    <span id="s-sp">50</span>/<span id="s-spm">50</span>
    <div style="width:120px;height:8px;background:#111"><div id="s-spb" style="width:100%;float:left;height:100%;background:#aaa"></div></div>
    Quest Points:<br />
    <span id="s-qp">5</span>/<span id="s-qpm">5</span>
    <div style="width:120px;height:8px;background:#111"><div id="s-qpb" style="width:100%;float:left;height:100%;background:#aaa"></div></div>
    Job:<br />
    <span id="s-jt">0</span>/<span id="s-jtm">0</span> minutes
    <div style="width:120px;height:8px;background:#111"><div id="s-jtb" style="width:0%;float:left;height:100%;background:#aaa"></div></div>
</center>`;
    playerInfo.querySelector('#s-close').addEventListener('click', closeSidebar);

    sidebar.appendChild(playerInfo);

    for (let [name, path] of [
        ['Chat', 'javascript:openChat()'],
        ['Main Page', '/home'],
        ['Travel'],
        ['Town'],
        ['Battle Arena', '/npcs/viewall'],
        ['World Bosses'],
        ['Notifications', '/events'],
        ['Messages', '/messages/inbox'],
        ['Guilds', '/guilds/menu'],
        ['Jobs', '/jobs/viewall'],
        ['Quests', '/quests/viewall'],
        ['Tasks', '/tasks/viewall'],
        ['Character'],
        ['Inventory'],
        ['Leaderboards'],
        ['Community'],
        ['Friends'],
        ['Your Profile', '/me'],
        ['Preferences'],
        ['About'],
        ['Support'],
    ]) {
        let a = document.createElement('a');
        a.style.color = IS_DARK_THEME ? 'white' : 'black';
        a.style.fontSize = '12pt';
        a.style.fontWeight = 'bold';
        a.style.whiteSpace = 'nowrap';
        a.style.margin = '3px 12px';
        a.href = path || '/' + name.replace(/ /g, '').toLowerCase();
        a.innerText = name;
        sidebar.appendChild(a);
    }

    // get player data
    let maxSteps = 50;
    [...sidebar.children].filter(e=>e.innerText === 'Messages')[0].id = 's-messages';
    [...sidebar.children].filter(e=>e.innerText === 'Notifications')[0].id = 's-notifications';
    function refreshSidebar() {
        xhr('get', '/mobapi', function (data) {
            document.getElementById('s-avatar').src = document.getElementById('s-avatar').src.replace(/[^/]+(?=\.png)/, data.avatar);
            document.getElementById('s-name').innerText = data.username;
            document.getElementById('s-level').innerText = data.level;
            document.getElementById('s-xp').innerText = data.exp;
            document.getElementById('s-xpm').innerText = data.max_exp;
            document.getElementById('s-xpb').style.width = data.exp_percent + '%';
            document.getElementById('s-hp').innerText = data.current_hp;
            document.getElementById('s-hpm').innerText = data.max_hp;
            document.getElementById('s-hpb').style.width = data.hp_percent + '%';
            document.getElementById('s-ep').innerText = data.energy;
            document.getElementById('s-epm').innerText = data.max_energy;
            document.getElementById('s-epb').style.width = data.energy_percent + '%';
            document.getElementById('s-sp').innerText = data.stepsleft;
            document.getElementById('s-spm').innerText = maxSteps = data.maxsteps;
            document.getElementById('s-spb').style.width = Math.round(100 * data.stepsleft / data.maxsteps) + '%';
            document.getElementById('s-messages').innerText = data.messages ? `Messages (${data.messages})` : 'Messages';
            document.getElementById('s-notifications').innerText = data.events ? `Notifications (${data.events})` : 'Notifications';
        });
    }

    refreshSidebar();
    setInterval(refreshSidebar, 10000);

    // quest points aren't in mobapi. sad
    let qp = 0,
        maxQp = 5,
        lastQpUpdate = 0,
        updateQpHandle = 0;
    function updateQp(newQp, increment=true) {
        clearTimeout(updateQpHandle);
        let qpUpdate = +new Date();
        if (newQp === undefined) {
            newQp = qp;
        }
        if (increment && newQp < maxQp && Math.floor(lastQpUpdate / 600000) !== Math.floor(qpUpdate / 600000)) {
            newQp++;
        }
        lastQpUpdate = qpUpdate;
        if (qp !== newQp) {
            document.getElementById('s-qp').innerText = newQp;
            document.getElementById('s-qpb').style.width = Math.round(100 * newQp / maxQp) + '%';
            qp = newQp;
        }
        updateQpHandle = setTimeout(updateQp, Math.max((600000 - qpUpdate % 600000) / 2, 1000));
    }
    xhr('get', '/quests/viewall', function (html) {
        let parser = new DOMParser();
        let doc = parser.parseFromString(html, 'text/html');
        qp = +doc.getElementById('questPoints').innerText;
        maxQp = +doc.getElementById('questPoints').parentNode.parentNode.nextElementSibling.children[1].innerText.match(/\d+/)[0];
        document.getElementById('s-qp').innerText = qp;
        document.getElementById('s-qpm').innerText = maxQp;
        document.getElementById('s-qpb').style.width = Math.round(100 * qp / maxQp) + '%';
        updateQp(qp, false);
    });


    let main = document.getElementsByClassName('container-two')[0];
    main.style.flex = '1 0 0';
    main.parentNode.style.display = 'flex';
    main.parentNode.style.flexFlow = 'row nowrap';
    main.parentNode.insertBefore(sidebarContainer, main);

    document.addEventListener('contextmenu', openSidebar);

    // keyboard shortcuts
    let chattext = document.getElementById('chatText');
    let send = document.getElementById('chattextBtn');
    document.addEventListener('keydown', function (event) {
        // TODO: more precise input checks
        if (document.activeElement.classList.contains('emojionearea-editor') && event.key === 'Enter' && event.ctrlKey) {
            chattext.value = chattext.emojioneArea.getText();
            send.click();
        }
        if (document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'INPUT' || document.activeElement.classList.contains('emojionearea-editor')) {
            return;
        }
        switch (event.key) {
            case 't':
                if (chatbox.style.width === '0px') {
                    openChat();
                } else {
                    closeChat();
                }
                break;
            case 's':
                if (sidebarVisible) {
                    closeSidebar();
                } else {
                    openSidebar();
                }
                break;
            case 'f':
                if (event.ctrlKey) {
                    if (!window.__cfRLUnblockHandlers) return false;
                    if (location.href.includes('/inventory')) {
                        inventoryFilter();
                        event.preventDefault();
                        event.stopPropagation();
                    } else if (location.href.includes('/market')) {
                        marketFilter();
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
                break;
        }
    });

    // updates jobs
    let jobFinish = 0,
        jobStart = 0;
    xhr('get', '/jobs/viewall', function (html) {
        // may not have job running
        try {
            let minutesLeft = +html.match(/(\d+)(?= minutes\.)/)[0],
                jobName = html.match(/[^>]+(?=<\/strong>)/)[0],
                gold = +html.match(/[\d,]+(?=\s*<img src="\/img\/icons\/S_Light01)/)[0].replace(/,/g, ''),
                nJobs = gold / JOB_GOLD_LOOKUP[jobName];
            document.getElementById('s-jtm').innerText = nJobs * 10;
            jobFinish = (+new Date()) + minutesLeft * 60000;
            jobStart = jobFinish - nJobs * 600000;
            let current = document.getElementById('s-jt'),
                bar = document.getElementById('s-jtb'),
                handle = 0;
            let update = function () {
                let mins = Math.min(nJobs * 10, Math.floor(((+new Date()) - jobStart) / 60000));
                current.innerText = mins;
                let percent = 10 * mins / nJobs;
                bar.style.width = percent + '%';
                if (percent === 100) {
                    clearInterval(handle);
                }
            }
            update();
            handle = setInterval(update, 10000);
            if (location.href.includes('/jobs/view')) {
                // might not have job running
                let minsNode = [...document.getElementsByTagName('strong')].find(el => el.innerText.includes('currently')).nextSibling.nextSibling;
                setInterval(function () {
                    minsNode.nodeValue = minsNode.nodeValue.replace(/\d+/, Math.ceil((jobFinish - new Date()) / 60000));
                    if (jobFinish - new Date() < 0) {
                        location.href += '';
                    }
                }, 10000);
            }
        } catch (e) {}
    });

    // monitor changes
    if (location.href.includes('/travel')) {
        document.getElementById('travel').style.position = 'relative';
        document.getElementById('travel').parentNode.style.height = '100%';

        let stepCounter = document.getElementById('s-sp');
        let stepBar = document.getElementById('s-spb');
        let stepObserver = new MutationObserver(function () {
            let steps = +document.getElementById('stepsleft').innerText;
            stepCounter.innerText = steps;
            stepBar.style.width = Math.round(100 * steps / maxSteps) + '%';
        });
        stepObserver.observe(document.getElementById('stepsleft'), {childList: true});
    }

    if (location.href.includes('/quests/viewall')) {
        let questObserver = new MutationObserver(function () {
            updateQp(qp - 1);
        });
        questObserver.observe(document.getElementById('questPoints'), {childList: true});
    }

    // refresh energy
    if (location.href.includes('/npcs/attack')) {
        document.getElementsByClassName('attackbg')[0].style.width = 'auto';
        let handler = setInterval(function () {
            try {
                unsafeWindow.attackNPC;
                clearInterval(handler);
                let attackNPC = eval('(' + (unsafeWindow.attackNPC + '').replace('function (data) {',`\
function (data) {
        refreshSidebar();
`) + ')');
                document.getElementById('attackButton').onclick = function () { attackNPC(); };
            } catch (e) {}
        }, 1000);
    }

    // return to travel on step npc's
    if (location.href.includes('/npcs/attack') && (+location.href.match(/\d+$/)[0]) > 284206 /* if it's less old than Ben Dover, the latest arena enemy */) {
        let npcSwalObserver = new MutationObserver(function () {
            document.getElementsByClassName('swal2-confirm')[0].addEventListener('click', function () {
                setTimeout(function () {
                    location.href = '/travel';
                }, 1000);
            });
        });
        npcSwalObserver.observe(document.body, {attributes: true});
        //document.body
    }

    // fix swal placeholders
    let swalObserver = new MutationObserver(function () {
        document.querySelectorAll('label.mdl-textfield__label').forEach(function (e) {
            e.previousElementSibling.placeholder = e.innerText;
            e.parentNode.removeChild(e);
        });
    });
    swalObserver.observe(document.body, {attributes: true});

    // fix dark mode
    if (IS_DARK_THEME && location.href.includes('/userlist/all')) {
        // TODO: auto quit modal
        let modal = document.getElementsByClassName('mdl-dialog')[0];
        modal.style.background = '#333';
        modal.style.color = 'white';
        modal.children[2].children[0].style.color = 'white';
        modal.children[2].children[1].style.color = 'white';
    }
})();