// ==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';
}
})();