Roll20 Fixes

Some silly fixes and 'improvements' to Roll20 because I'm impatient and a psycho

As of 02. 10. 2018. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name			Roll20 Fixes
// @namespace		http://statonions.com/
// @version			0.3.4
// @description		Some silly fixes and 'improvements' to Roll20 because I'm impatient and a psycho
// @author			Justice Noon
// @match			https://app.roll20.net/editor/
// @grant			GM_setValue
// @grant			GM_getValue
// ==/UserScript==

(function() {
    'use strict';

//Features
var TMp = {
	clickShow: true,
	macroHide: true,
	fullButt: true,
	fixPing: "1",
	css: {
		player:	false,
		zoom: false,
		sidebar: false,
		quick: false,
		turns: false,
		macroTok: true
	},
	pfCustomAttr: true,
	splitTokName: "3",
	toolbarButt: true,
	scaleToks: false,
    statusHQ: false
};
try {
	GM_info;
	var stored = GM_getValue('TMp', 'nah');
	if (stored == 'nah')
		GM_setValue('TMp', JSON.stringify(TMp));
	else {
		var checkVals = function(d, s) {
			_.each(d, function(v,k) {
				if (typeof d[k] !== typeof s[k])
					s[k] = d[k];
				if (typeof d[k] == 'object')
					checkVals(d[k], s[k]);
			});
		};
		checkVals(TMp, JSON.parse(stored));
		GM_setValue('TMp', JSON.stringify(TMp));
	}
	checkLoaded();
}
catch(err) {
	checkLoaded();
}

function checkLoaded(tried) {
    if (!tried) tried = 1;
    try {
            if (Campaign && Campaign.gameFullyLoaded) {
                debugger;
                tried = 69;
				showtime();
			}
            else
                setTimeout(checkLoaded, 1000, tried);
    }
    catch(err) {
        if (++tried < 69)
            setTimeout(checkLoaded, 1000, tried);
        else {
            console.log(err);
			console.log("Couldn't load userscript. Aborting.");
		}
    }
};

function showtime() {
//For *my* players specifically. Activates all css tweaks. I'm too lazy to maintain separate code (in-person touchpad view)
////_.each(TMp.css, (c,i,l) => l[i] = true);
	//Create greasemonkey preferences menu
	try {
		GM_info;
		var prefLi = document.createElement('li');
		prefLi.id = 'showfixes';
		var prefSpan = document.createElement('span');
		prefSpan.setAttribute('class', 'pictos');
		prefSpan.appendChild(document.createTextNode('x'));
		prefLi.appendChild(prefSpan);
		prefLi.appendChild(document.createTextNode("\r\n        'Fixes' Preferences\r\n"));
		document.getElementById('helpsite').childNodes[3].childNodes[1].appendChild(prefLi);
		prefLi.addEventListener('click', promptPrefs);

		function promptPrefs(e) {
			e.stopPropagation();
			var finished = false, response, ref = TMp, counter, changeKey;
			while (!finished) {
				counter = 1;
				response = parseInt(prompt(_.reduce(ref, (memo, v, k) => memo + '\n' + counter++ + ':' + k + '  ::  ' + (_.isObject(v) ? '[' + _.keys(v).length + ' properties]' : v), 'Enter a number:\n0:Back'), 'Enter a number to swap value.'));
				if (_.isNaN(response) || response == 0 || response > counter) {
					if (ref === TMp) {
						GM_setValue('TMp', JSON.stringify(TMp));
						finished = true;
						alert('Reload to see changes');
					}
					else
						ref = TMp;
				}
				else {
					changeKey = _.keys(ref)[response-1];
					if (_.isObject(ref[changeKey]))
						ref = ref[changeKey];
					else if (_.isBoolean(ref[changeKey]))
						ref[changeKey] = !ref[changeKey];
					else
						ref[changeKey] = prompt('Enter a new value for ' + changeKey);
				}
			}
		}
	}
	catch (err) {
		console.log('No GM. No need for preferences.');
	}

	//Display tokenname on select
	if (TMp.clickShow) {
		var toks = [];
		var muteCallback = function(mutationsList) {
			for(var mutation of mutationsList) {
				if (!_.isEmpty(mutation.addedNodes)) {
					let cct = currentcontexttarget.canvas;
					let cctIt = cct.lastRenderedObjectWithControlsAboveOverlay.model.attributes;
					if (!cctIt.showname) {
						cctIt.showname = true;
						toks.push([cctIt.page_id, cctIt.id]);
					}
					cct._objects.push(cct._objects.splice(cct._objects.indexOf(cct.lastRenderedObjectWithControlsAboveOverlay), 1)[0]);
					currentcontexttarget.canvas.renderAll && currentcontexttarget.canvas.renderAll();
				}
				else if (!_.isEmpty(mutation.removedNodes)) {
					_.each(toks, tok => {if (tok[0] == Campaign.activePage().id) _.find(Campaign.activePage().thegraphics.models, tokk => tokk.attributes.id == tok[1]).attributes.showname = false});
					toks = [];
					currentcontexttarget.canvas.renderAll && currentcontexttarget.canvas.renderAll();
					Campaign.activePage().reorderByZ();
				}
			}
		};
		var observer = new MutationObserver(muteCallback);
		observer.observe(document.getElementById('editor-wrapper'), {childList: true});
	}

	if (TMp.css.macroTok) {
		var macroTokCallback = function(mutationsList) {
			for (var mutation of mutationsList) {
                _.delay((mutation) => {
                    let cctIt = currentcontexttarget.canvas.lastRenderedObjectWithControlsAboveOverlay.model.attributes;
                    if (mutation.target.style.display == 'block' && cctIt.gmnotes != 'blank' && cctIt.gmnotes != '%3Cp%3Eblank%3C/p%3E')
                        _.each(document.getElementsByClassName('tokenactions')[0].firstElementChild.children, ob => ob.setAttribute('data-blank', 'false'));
                }, (typeof currentcontexttarget == 'undefined' ? 1000 : 0), mutation);
			}
		};
		var macroObserver = new MutationObserver(macroTokCallback);
		macroObserver.observe(document.querySelector('.mode.tokenactions'), {attributeFilter: ['style']});
	}

	//Allow Macros to be hidden
	if (TMp.macroHide) {
		var toggleMacro = document.createElement('div');
		toggleMacro.setAttribute('class', 'macrobox');
		var toggleButton = document.createElement('button');
		toggleButton.setAttribute('class', 'btn');
		toggleButton.appendChild(document.createTextNode('Show/Hide'));
		toggleButton.id = 'toggleMacros';
		toggleMacro.appendChild(toggleButton)
		//macrobar is defined by default
		macrobar.appendChild(toggleMacro);
		document.getElementById('toggleMacros').addEventListener('click', () => macrobar.style.left = (macrobar.style.left == '' ? (macrobar.offsetWidth * -1 + 100) + 'px' : ''));
	}

	//Create fullscreen button
	if (TMp.fullButt) {
		var fullScreenLi = document.createElement('li');
		fullScreenLi.setAttribute('tip', 'Toggle Fullscreen');
		fullScreenLi.id = 'fullscreener';
		var fullScreenSpan = document.createElement('span');
		fullScreenSpan.setAttribute('class', 'pictos');
		fullScreenSpan.appendChild(document.createTextNode('`'));
		fullScreenLi.appendChild(fullScreenSpan);
		floatingtoolbar.childNodes[1].appendChild(fullScreenLi);
		fullScreenLi.addEventListener('click', toggleFullScreen);

		function toggleFullScreen() {
			var doc = window.document;
			var docEl = doc.documentElement;

			var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
			var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;

			if(!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
			requestFullScreen.call(docEl);
			}
			else {
			cancelFullScreen.call(doc);
			}
		}
	}
	//Create Floating Toolbar button
	if (TMp.toolbarButt) {
		var ToolBar = _.find(Campaign.players.get(d20_player_id).macros.models, m => m.attributes.name == 'ToolBar');
		if (_.isUndefined(ToolBar))
			ToolBar = _.find(_.union(..._.map(Campaign.players.models, m => m.macros.models)), m => {return m.visibleToCurrentPlayer() && m.attributes.name == 'ToolBar'});
		if (ToolBar) {
			var actions = ToolBar.attributes.action.split('\n');
			var toolBarLi = document.createElement('li');
			toolBarLi.setAttribute('tip', 'Additional Macros');
			toolBarLi.id = 'toolBar';
			for (var k = 0, thisAction, thisSpan, thisLi, thisA, thisODiv; k < actions.length; k++) {
				thisAction = actions[k].split('||');
				thisSpan = document.createElement('span');
				thisSpan.setAttribute('class', 'pictos');
				thisSpan.appendChild(document.createTextNode(thisAction[0]));
				if (k == 0) {
					thisSpan.setAttribute('style', 'margin: -1em;');
					toolBarLi.appendChild(thisSpan);
					var thisDiv = document.createElement('div');
					thisDiv.setAttribute('class', 'submenu');
					var thisUl = document.createElement('ul');
				}
				else {
					thisLi = document.createElement('li');
					thisA = document.createElement('a');
                    if (thisAction[2].indexOf('j:') > -1) {
                     thisA.setAttribute('href', '!');
                     thisA.onclick = eval('() => {' + thisAction[2].substr(2) + '}');
                    }
                    else
                        thisA.setAttribute('href', thisAction[2]);
					thisA.appendChild(thisSpan);
					thisODiv = document.createElement('div');
					thisODiv.appendChild(document.createTextNode(thisAction[1]));
					thisA.appendChild(thisODiv);
					thisLi.appendChild(thisA);
					thisUl.appendChild(thisLi);
				}
			}
			thisDiv.appendChild(thisUl);
			thisDiv.setAttribute('style', 'top: -' + k*60 + '%;');
			toolBarLi.appendChild(thisDiv);
			floatingtoolbar.childNodes[1].appendChild(toolBarLi);
		}
	}

	//Fix Pings
	if (TMp.fixPing != "0") {
		JSON.parse2 = JSON.parse;
		JSON.parse = function(e) {
			var intercept = JSON.parse2(e), api = false;
			if (intercept.type == 'mapping') {
				if (_.isUndefined(intercept.currentLayer))
                    api = true;
                if (TMp.fixPing == "2" && intercept.player == "api") {
                    if (is_gm)
                        spoofPing(intercept);
                    Object.assign(intercept, {scrollto: false, left: -100, top: -100});
                }
                intercept.currentLayer = 'objects';
                if (intercept.pageid != Campaign.activePage().id || (api && intercept.player != "api" && intercept.player != d20_player_id))
                    Object.assign(intercept, {scrollto: false, left: -100, top: -100});
			}
			return intercept;
		};
		function spoofPing(old) {
			var paddingOffset = [], currentCanvasOffset = [];
			var canvasZoom = Math.round(document.getElementById('maincanvas').getContext('2d').getTransform().a * 10) / 10;
			var canvasWidth = document.getElementById('maincanvas').clientWidth, canvasHeight = document.getElementById('maincanvas').clientHeight;
			var scaledTop = document.getElementById('editor-wrapper').scrollTop / canvasZoom, scaledLeft = document.getElementById('editor-wrapper').scrollLeft / canvasZoom;
			var pageWidth = Campaign.activePage().attributes.width * 70, pageHeight = Campaign.activePage().attributes.height * 70;
			var padding = 125;
			if (scaledLeft < padding)
				(paddingOffset[0] = padding - scaledLeft, currentCanvasOffset[0] = 0);
			else {
				if (pageWidth / canvasZoom - scaledLeft - canvasWidth / canvasZoom + padding < 0)
					(paddingOffset[0] = pageWidth / canvasZoom - scaledLeft - canvasWidth / canvasZoom + padding, currentCanvasOffset[0] = scaledLeft - padding + paddingOffset[0]);
				else
					(paddingOffset[0] = 0, currentCanvasOffset[0] = scaledLeft - padding);
			}
			if (scaledTop < padding)
				(paddingOffset[1] = padding - scaledTop, currentCanvasOffset[1] = 0);
			else {
				if (pageHeight / canvasZoom - scaledTop - canvasHeight / canvasZoom + padding < 0)
					(paddingOffset[1] = pageHeight / canvasZoom - scaledTop - canvasHeight / canvasZoom + padding, currentCanvasOffset[1] = scaledTop - padding + paddingOffset[1]);
				else
					(paddingOffset[1] = 0, currentCanvasOffset[1] = scaledTop - padding);
			}
			//var m = {left: old.left, top: old.top};
            var pos = {left: canvasZoom * (old.left - currentCanvasOffset[0]) + paddingOffset[0], top: canvasZoom * (old.top - currentCanvasOffset[1]) + paddingOffset[1]};
			var evt = new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true, buttons: 1, shiftKey: true, clientX: pos.left, clientY: pos.top});
            var maskedArr = [];
           _.each(Campaign.activePage().thegraphics.models, m => {
               if (m.attributes.layer == 'objects' && Math.abs(m.attributes.top - old.top) <= m.attributes.height / 2 && Math.abs(m.attributes.left - old.left) <= m.attributes.width / 2) {
                   //debugger;
                   maskedArr.push([m.id, m.view.graphic.selectable]);
                   m.view.graphic.selectable = false;
               }
           });
            _.delay((e, mask) => {document.getElementById('upperCanvas').dispatchEvent(e); _.each(mask, m => Campaign.activePage().thegraphics._byId[m[0]].view.graphic.selectable = m[1]);}, 1000, new MouseEvent('mouseup', {bubbles: true, cancelable: true, composed: true, buttons: 1, clientX: 1, clientY: 1}), maskedArr);
            return !(document.getElementById('upperCanvas').dispatchEvent(evt));
		}
	}

	//CSS override block
	if (_.contains(TMp.css, true) || TMp.toolbarButt) {
		var style = document.createElement('style');
		style.type = 'text/css';
		style.innerHTML = '';
		//Remove player names/ icons
		if (TMp.css.player)
			style.innerHTML += '.player.ui-droppable {display: none;} ';
		//Remove Zoom Slider
		if (TMp.css.zoom)
			style.innerHTML += '#zoomslider {display:none;} ';
		//Remove sidebar show/ hide button
		if (TMp.css.sidebar)
			style.innerHTML += '#sidebarcontrol {display: none;} ';
		//Increase quickmenu sizes
		if (TMp.css.quick)
			style.innerHTML += '.sheet-roll-cell>a{padding:.5em !important;} ';
		//Permanently hide turn order
		if (TMp.css.turns)
			style.innerHTML += 'div.ui-dialog-buttons[style*="width: 160px;"] {display: none !important;} ';
		//Hide macroTokens by default
		if (TMp.css.macroTok)
			style.innerHTML += 'button[data-type="macro"]:not([data-blank="false"]) {display: none !important;} ';
		//Fills Toolbar buttons to their spaces
			style.innerHTML += '.submenu > ul > li > a {display: inline-block; width: 100%; height: 100%; color: grey; text-align: center;} .submenu > ul > li > a:hover {text-decoration: none;} .submenu > ul > li > a > .pictos {position: relative; left: -6rem; top: .1rem;} .submenu > ul > li > a > div {position: absolute; left: 3rem; top: .5rem;}';
		document.getElementsByTagName('head')[0].appendChild(style);
	}

	//Character sheets display custom attributes as name
	if (TMp.pfCustomAttr) {
		function cssInjection(charId) {
			var list = [1,2,3,10,11,12];
			var head = 'div.dialog.characterdialog[data-characterid=' + charId + '] {', foot = '';
			_.each(list, function(cK) {
				cK = 'customa' + cK;
				let val = {};
				if (!_.isUndefined(val = _.find(Campaign.characters._byId[charId].attribs.models, at => at.attributes.name == cK + '-name')))
					head += `--${cK}: "${val.attributes.current}"; `
				foot += `input[title="@{buff_${cK}-total}"] + span {visibility: hidden; top: -1.2rem;} input[title="@{buff_${cK}-total}"] + span::after {content: var(--${cK}, "${cK}"); visibility: visible; display: block;} `
			});
			return [head + '} ', foot];
		}
		function newShow(that) {
			var customOver;
			var modCss = cssInjection(that.model.attributes.id);
			if (_.isNull(document.getElementById('customOver'))) {
				customOver = document.createElement('style');
				customOver.type = 'text/css';
				customOver.id = 'customOver';
				customOver.innerHTML = modCss[1];
			}
			else {
				customOver = document.getElementById('customOver');
			}
			customOver.innerHTML += (modCss[0].indexOf('{}') > -1 ? '' : modCss[0]);
			if (that.childWindow)
				_.delay(function() {customOver.innerHTML = modCss.join(' '); window.allChildWindows[0].document.getElementsByTagName('head')[0].appendChild(customOver)}, 2000);
			else
				document.getElementsByTagName('head')[0].appendChild(customOver);
		};
		_.each(Campaign.characters.models, ch => {ch.view.showDialog2 = ch.view.showDialog; ch.view.showDialog = function(e, t) {this.showDialog2(e, t); newShow(this)}});
		_.each(Campaign.characters.models, ch => {ch.view.showPopout2 = ch.view.showPopout; ch.view.showPopout = function(e, t) {this.showPopout2(e, t); newShow(this)}});
		_.each(window.allChildWindows, win => {newShow(Campaign.characters._byId[win.document.getElementsByClassName('dialog characterdialog')[0].getAttribute('data-characterid')].view)});
	}

	//Tokens auto split their name to multiple lines
	if (TMp.splitTokName != "0") {
        var newFill = function(x, y, w, h) {
            if (this.font != 'bold 14px Arial' || this.fillStyle.replace(/ /g,'').indexOf('rgba(255,255,255,0.5') == -1)
                this.fillRect2(x, y, w, h);
        };
        var newText = function(t, x, y, m) {
            if (this.font != 'bold 14px Arial' || this.fillStyle != '#000000')
                this.fillText2(t, x, y, m);
            else if (this.canvas.id == 'upperCanvas')
                return;
            else {
                var g = 14, r = y - 22, n = m, max = parseInt(TMp.splitTokName), forced;
                var texes = t.split(' ');
                for (var k = 0; k < texes.length; k++) {
                    while ((this.measureText(texes[k] + ' ' + texes[k+1]).width < r*2 || k >= max - 1) && k+1 < texes.length ) {
                        texes[k] += ' ' + texes[k+1];
                        texes.splice(k+1, 1);
                    }
                    if (texes[k].indexOf('\\n') > -1) {
                        forced = texes[k].split('\\n');
                        texes[k] = forced.shift();
                        texes.splice(k+1, 0, ...forced);
                    }
                    texes[k] = texes[k].replace(/_/g, ' ');
                    n = this.measureText(texes[k]).width;
                    this.fillStyle = 'rgba(255, 255, 255, 0.50)';
                    this.fillRect2(-1 * Math.floor((n + 6) / 2), r + 8 + (g+6)*k, n + 6, g + 6);
                    this.fillStyle = 'rgb(0,0,0)';
                    this.fillText2(texes[k], x, y + (g+6)*k, n);
                }
            }
		};
        _.each(['maincanvas', 'upperCanvas'], function(c) {
            let can = document.getElementById(c).getContext('2d');
            can.fillRect2 = can.fillRect;
            can.fillRect = newFill;
            can.fillText2 = can.fillText;
            can.fillText = newText;
        });
	}

	if (TMp.scaleToks || TMp.statusHQ) {
		var newRect = function(x, y, w, h) {
			var rgba = /rgba\( ?(\d*) ?, ?(\d*) ?, ?(\d*) ?, ?((?:\d|\.)*) ?\)/.exec(this.fillStyle), fillStr, offset = 24;
			if (rgba && parseFloat(rgba[4]).toFixed(2) == "0.75" && (Campaign.tokendisplay.bar3_rgb + '' == (fillStr = rgba[1] + ',' + rgba[2] + ',' + rgba[3]) || (Campaign.tokendisplay.bar2_rgb + '' == fillStr && _.isNumber(offset -= 12)) || (Campaign.tokendisplay.bar1_rgb + '' == fillStr && _.isNumber(offset -= 24)))) {
				var scale = (x - 3) * -1 / 35;
				if (scale != 1) {
					h = scale * 8;
					y = (y - offset + 20) + ((offset - 20) * scale);
					x = (x - 3) + (3 * scale);
					this.lineWidth = scale;
					w = Math.floor((70 * scale - 6 * scale) * (w / (70 * scale - 6)));
				}
			}
			this.rect2(x, y, w, h);
		};
		var newImage = function() {
			if (arguments[7] && arguments[7] == 21 && (arguments[0].src == 'https://app.roll20.net/images/statussheet.png' || arguments[0].src == 'https://app.roll20.net/images/statussheet_small.png')) {
                if (TMp.statusHQ)
                    arguments[0].src = 'https://my.mixtape.moe/ycpyrx.svg';
			}
			this.drawImage2(...arguments);
		}
		_.each(['maincanvas', 'upperCanvas'], function(c) {
			let can = document.getElementById(c).getContext('2d');
			if (TMp.scaleToks) {
				can.rect2 = can.rect;
				can.rect = newRect;
			}
			can.drawImage2 = can.drawImage;
			can.drawImage = newImage;
		});
	}
}

})();