您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extends and enhances Roll20's keyboard shortcuts.
// ==UserScript== // @name Roll20 Enhanced Keyboard Shortcuts // @namespace http://tampermonkey.net/ // @version 1.8.2 // @license MIT // @description Extends and enhances Roll20's keyboard shortcuts. // @author Jon Molnar // @match *://app.roll20.net/editor/ // @match *://app.roll20dev.net/editor/ // @icon https://www.google.com/s2/favicons?domain=tampermonkey.net // @grant none // ==/UserScript== ;(function() { 'use strict'; // Utilities const LOG = true; function log(...args) { if (LOG) console.log("[REKS]", ...args); } function waitForDeps(deps, callback) { log("Waiting for dependencies:", deps); function _waitForDeps() { setTimeout(() => { if (deps.some(dep => window[dep] === undefined)) { _waitForDeps(); } else { callback(); } }, 10); } _waitForDeps(); } waitForDeps(["jQuery"], () => { const jQuery = window.jQuery; const $ = jQuery; log("Inlining jQuery.simulate()"); /*! * jQuery Simulate v@VERSION - simulate browser mouse and keyboard events * https://github.com/jquery/jquery-simulate * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * Date: @DATE */ ;(function( $, undefined ) { var rkeyEvent = /^key/, rdashAlpha = /-([a-z])/g, rmouseEvent = /^(?:mouse|contextmenu)|click/; function fcamelCase( _all, letter ) { return letter.toUpperCase(); } function camelCase( string ) { return string.replace( rdashAlpha, fcamelCase ); } $.fn.simulate = function( type, options ) { return this.each(function() { new $.simulate( this, type, options ); }); }; $.simulate = function( elem, type, options ) { var method = camelCase( "simulate-" + type ); this.target = elem; this.options = options; if ( this[ method ] ) { this[ method ](); } else { this.simulateEvent( elem, type, options ); } }; $.extend( $.simulate, { keyCode: { BACKSPACE: 8, COMMA: 188, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, LEFT: 37, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SPACE: 32, TAB: 9, UP: 38 }, buttonCode: { LEFT: 0, MIDDLE: 1, RIGHT: 2 } }); $.extend( $.simulate.prototype, { simulateEvent: function( elem, type, options ) { var event = this.createEvent( type, options ); this.dispatchEvent( elem, type, event, options ); }, createEvent: function( type, options ) { if ( rkeyEvent.test( type ) ) { return this.keyEvent( type, options ); } if ( rmouseEvent.test( type ) ) { return this.mouseEvent( type, options ); } }, mouseEvent: function( type, options ) { var event, eventDoc, doc, body; options = $.extend({ bubbles: true, cancelable: (type !== "mousemove"), view: window, detail: 0, screenX: 0, screenY: 0, clientX: 1, clientY: 1, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0, relatedTarget: undefined }, options ); if ( document.createEvent ) { event = document.createEvent( "MouseEvents" ); event.initMouseEvent( type, options.bubbles, options.cancelable, options.view, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, options.relatedTarget || document.body.parentNode ); // IE 9+ creates events with pageX and pageY set to 0. // Trying to modify the properties throws an error, // so we define getters to return the correct values. if ( event.pageX === 0 && event.pageY === 0 && Object.defineProperty ) { eventDoc = event.relatedTarget.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; Object.defineProperty( event, "pageX", { get: function() { return options.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); } }); Object.defineProperty( event, "pageY", { get: function() { return options.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } }); } } else if ( document.createEventObject ) { event = document.createEventObject(); $.extend( event, options ); // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx // so we actually need to map the standard back to oldIE event.button = { 0: 1, 1: 4, 2: 2 }[ event.button ] || ( event.button === -1 ? 0 : event.button ); } return event; }, keyEvent: function( type, options ) { var event; options = $.extend({ bubbles: true, cancelable: true, view: window, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, keyCode: 0, charCode: undefined }, options ); if ( document.createEvent ) { try { event = document.createEvent( "KeyEvents" ); event.initKeyEvent( type, options.bubbles, options.cancelable, options.view, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode ); // initKeyEvent throws an exception in WebKit // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution // and also https://bugs.webkit.org/show_bug.cgi?id=13368 // fall back to a generic event until we decide to implement initKeyboardEvent } catch( err ) { event = document.createEvent( "Events" ); event.initEvent( type, options.bubbles, options.cancelable ); $.extend( event, { view: options.view, ctrlKey: options.ctrlKey, altKey: options.altKey, shiftKey: options.shiftKey, metaKey: options.metaKey, keyCode: options.keyCode, charCode: options.charCode }); } } else if ( document.createEventObject ) { event = document.createEventObject(); $.extend( event, options ); } if ( !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ) || (({}).toString.call( window.opera ) === "[object Opera]") ) { event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode; event.charCode = undefined; } return event; }, dispatchEvent: function( elem, type, event ) { if ( elem.dispatchEvent ) { elem.dispatchEvent( event ); } else if ( type === "click" && elem.click && elem.nodeName.toLowerCase() === "input" ) { elem.click(); } else if ( elem.fireEvent ) { elem.fireEvent( "on" + type, event ); } }, simulateFocus: function() { var focusinEvent, triggered = false, element = $( this.target ); function trigger() { triggered = true; } element.on( "focus", trigger ); element[ 0 ].focus(); if ( !triggered ) { focusinEvent = $.Event( "focusin" ); focusinEvent.preventDefault(); element.trigger( focusinEvent ); element.triggerHandler( "focus" ); } element.off( "focus", trigger ); }, simulateBlur: function() { var focusoutEvent, triggered = false, element = $( this.target ); function trigger() { triggered = true; } element.on( "blur", trigger ); element[ 0 ].blur(); // blur events are async in IE setTimeout(function() { // IE won't let the blur occur if the window is inactive if ( element[ 0 ].ownerDocument.activeElement === element[ 0 ] ) { element[ 0 ].ownerDocument.body.focus(); } // Firefox won't trigger events if the window is inactive // IE doesn't trigger events if we had to manually focus the body if ( !triggered ) { focusoutEvent = $.Event( "focusout" ); focusoutEvent.preventDefault(); element.trigger( focusoutEvent ); element.triggerHandler( "blur" ); } element.off( "blur", trigger ); }, 1 ); } }); /** complex events **/ function findCenter( elem ) { var offset, document = $( elem.ownerDocument ); elem = $( elem ); offset = elem.offset(); return { x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(), y: offset.top + elem.outerHeight() / 2 - document.scrollTop() }; } function findCorner( elem ) { var offset, document = $( elem.ownerDocument ); elem = $( elem ); offset = elem.offset(); return { x: offset.left - document.scrollLeft(), y: offset.top - document.scrollTop() }; } $.extend( $.simulate.prototype, { simulateDrag: function() { var i = 0, target = this.target, eventDoc = target.ownerDocument, options = this.options, center = options.handle === "corner" ? findCorner( target ) : findCenter( target ), x = Math.floor( center.x ), y = Math.floor( center.y ), coord = { clientX: x, clientY: y }, dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ), dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ), moves = options.moves || 3; this.simulateEvent( target, "mousedown", coord ); for ( ; i < moves ; i++ ) { x += dx / moves; y += dy / moves; coord = { clientX: Math.round( x ), clientY: Math.round( y ) }; this.simulateEvent( eventDoc, "mousemove", coord ); } if ( $.contains( eventDoc, target ) ) { this.simulateEvent( target, "mouseup", coord ); this.simulateEvent( target, "click", coord ); } else { this.simulateEvent( eventDoc, "mouseup", coord ); } } }); })( jQuery ); // END INLINED JQUERY.SIMULATE log("Roll20 Enhanced Keyboard Shortcuts is starting up..."); const textBox = $('<input type="text" class="eks eks-input">') const K_START = "\\"; const style = $(` <style> .eks { position: absolute; left: 50%; z-index: 99999; width: 500px; margin-left: -256px; padding: 12px; border: none; border-radius: 8px; background-color: #000; color: #fff; opacity: 0.7; } .eks-input { top: -42px; font-size: 20px; text-align: center; transition: top 0.2s; } .eks-input:focus { top: 20px; } .eks-help { top: 80px; } .eks-help pre { background-color: #000; color: #fff; border: none; max-height: calc(100vh - 256px); overflow-y: auto; } .eks-help button { position: absolute; top: 12px; right: 12px; border: none; background-color: #000; color: #fff; font-size: 20px; } </style> `); const help_html = ` <div class="eks eks-help"> <button>x</button> <pre> ${K_START}h Show this help ${K_START}g- Page Controls ${K_START}ga<pagename> Send players & GM to the named page ${K_START}gp<pagename> Send players only to the named page ${K_START}gg<pagename> Send GM only to the named page Notes: - Page names are case-sensitive. - Only players on the bookmark will be moved. ${K_START}gs<settings> Set page settings Settings are formatted as <key>=<value> pairs and separated by commas. dl (y|n) Dynamic lighting xm (y|n) Explorer mode gi (y|n) Global illumination (daylight mode) ud (y|n) Update token vision on drop rm (y|n) Restrict movement by dynamic lighting barriers fw (y|n) Standard fog of war ge (y|n) Grid enabled gs (number) Grid scale gu (unit) Grid unit Units: ft, m, km, mi, in, cm, un, hex, sq, custom gt (s|v|h) Grid type s = square, v = vert. hex, h = horiz. hex ${K_START}c Chat - Send any message, including commands. Example: ${K_START}c/w gm Roll with advantage: [[2d20kh1]] Note: This command doesn't work with chat popped out. ${K_START}r<dice-expression> Roll dice All Roll20 dice expressions are supported. Examples: 2d20kh1 Roll 2d20, keep highest 4d6dl1 Roll 4d6, drop lowest 5d6>4 Roll 5d6, count 4+ as success d6!+d8! Roll 1d6 + 1d8, explode both dice ?{#}dF Prompt for a number and roll that many fudge dice ${K_START}a- Audio Note: These commands don't work with the Jukebox popped out. ${K_START}as Stop all audio ${K_START}an Play next track ${K_START}ap<name> Play playlist (case sensitive) Example: ${K_START}\apCombat Leave name blank to be prompted, i.e. \ap ${K_START}at<name> Play track (case sensitive) Example: ${K_START}\atRain - Heavy Leave name blank to be prompted, i.e. \at ${K_START}n- Create new things ${K_START}nh New handout ${K_START}nc New character ${K_START}nt New rollable table ${K_START}t- Turn tracker ${K_START}to Open turn tracker ${K_START}tc Clear turn tracker ${K_START}tx Close turn tracker ${K_START}tn Next turn ${K_START}ts Sort turns ${K_START}l<bright>[/<dim>] Lighting Set the lighting on the currently selected token. Example: ${K_START}l25/25 ${K_START}v- Vision ${K_START}vn Enable normal vision ${K_START}vx Blind (disable vision) ${K_START}vd<dist> Darkvision ${K_START}s[name] Toggle named status marker on selected token If no name is provided, clear all status markers ${K_START}o<options> Set options for selected token Options are formatted as <key>=<value> pairs and separated by commas. Example: ${K_START}o b1a=hp,b2a=ac Note: For any option value, you can enter ? to be prompted, e.g. ${K_START}o nv=y,np=y,nt=? Bars: (# = 1, 2, 3, or * (all bars)) b#c (number) Bar current value b#m (number) Bar max value b#a (attribute) Bar linked attribute b#v (y|n) Bar visible to players b#e (y|n) Bar editable by controlling player b#t (h|e|*) Bar text overlay visibility h = hidden, e = editors, * = everyone bl (a|b|ot|ob) Token bar location: a = above, b = below, ot = overlap top, ob = overlap bottom bs (s|c) Token bar style: s = standard, c = compact Auras: (# = 1, 2, or * (both auras)) a#r (number) Aura radius a#s (c|s) Aura shape: c = circle, s = square a#v (y|n) Aura visible to players a#e (y|n) Aura editable by controlling player Global permissions: *v (y|n) Bars/auras/name visible to players *e (y|n) Bars/auras/name editable by controlling player Misc: nv (y|n) Token name visible to players ne (y|n) Token name editable by controlling player np (y|n) Nameplate visible below token nt (text) Name text tv (y|n) Tooltip visible on hover tt (text) Tooltip text ${K_START}m- Run macros ${K_START}mcs Start combat ${K_START}mce End combat ${K_START}p Preferences Preferences are formatted as <key>=<value> pairs and separated by commas. Example: ${K_START}p aks=y,3dd=y,a3d=y,pas=n,cht=n aks (y|n) Advanced keyboard shortcuts wpc (y|n) Window popouts for characters bcb (y|n) Background chat beep add (y|n) Advanced dice 3dd (y|n) Use 3D dice a3d (y|n) Autoroll 3D dice cav (y|n) Chat avatars cts (y|n) Chat timestamps sta (y|n) Sort token actions ani (y|n) Animated graphics ust (z|p) Use scroll to... z = zoom, p = pan smp (t|b|l|r) Status marker position t = top, b = bottom, l= left, r = right pas (l|r|s|n) Player avatar size l = large, r = regular, s = small, n = names only cht (w|l|n) Chat tech w = WebRTC, n = none </pre></div>`; function yn (v) { switch (v.toLowerCase()) { case "y": case "yes": case "1": case "on": return true; default: return false; } }; function check_uncheck(jq, checked) { if (jq.prop("checked") !== checked) { jq.click(); } } function unknown_cmd(command) { alert("Unknown command: " + K_START + command + " Try " + K_START + "h for help"); } // Turn Tracker function clear_turn_tracker() { $(".clearlist").click(); $(".ui-dialog :contains(sure you want to clear the turns)").parent().find("button:contains(Cancel)").prev().click(); } function close_turn_tracker() { $(".ui-dialog-title:contains(Turn Order)").next().click(); } function open_turn_tracker() { $("#startrounds").click(); } // Audio controls function stop_audio_async(callback) { let interval; let do_stop = () => { const btn = $("#jukeboxwhatsplaying .play"); if (btn.length > 0) { btn.click(); } else { clearInterval(interval); if (callback) callback(); } }; interval = setInterval(do_stop, 200); } function play_playlist(title) { if (!title) { title = prompt("Playlist title:"); } $("#jukeboxfolderroot .folder-title:contains(" + title + ")") .next() .find(".play") .click(); } function play_track(title) { if (!title) { title = prompt("Track title:"); } $("#jukeboxfolderroot .title:contains(" + title + ")") .next() .find(".play") .click(); } function next_track() { $("#jukeboxwhatsplaying .plnext").click(); } // Dice function roll_dice(dice) { const ctx = $(".recentroll").first(); ctx.find(".formula").text(dice); ctx.find("button").click(); } // Token Settings function open_token_settings_async(callback) { const token_settings_btn = $("[data-action-type=tokensettings]"); if (token_settings_btn.length !== 1) { alert("Error: Select one token before using this command."); } else { token_settings_btn.click(); if (callback) { setTimeout(() => { const id = $("[data-tokenid]").last().attr("data-tokenid"); callback(id); }, 10); } } } function save_token_settings(id) { $("[data-tokenid="+id+"]").parent().find(".btn-primary").click(); } function set_token_vision(id, bool) { if (!bool) set_token_dark_vision(id, 0); $("[data-tokenid="+id+"] .dyn_fog_emits_vision").prop("checked", bool); } function set_token_dark_vision(id, dist) { if (dist > 0) set_token_vision(id, true); $("[data-tokenid="+id+"] .dyn_fog_emits_dark_vision").prop("checked", dist > 0); $("[data-tokenid="+id+"] .dyn_fog_dark_vision_range").val(dist); } function set_token_bright_light(id, dist) { $("[data-tokenid="+id+"] .dyn_fog_emits_light").prop("checked", dist ? true : false); $("[data-tokenid="+id+"] .dyn_fog_light_range").val(dist); } function set_token_dim_light(id, dist) { $("[data-tokenid="+id+"] .dyn_fog_emits_dim_light").prop("checked", dist ? true : false); $("[data-tokenid="+id+"] .dyn_fog_dim_light_range").val(dist); } function set_token_bar_attribute(token_id, bar_num, attribute_name) { const select = $(`[data-tokenid=${token_id}] .bar${bar_num}_link`); const opts = select.find("option").get().filter(e => e.innerHTML === attribute_name); if (opts.length > 0) { select.val(opts[0].value); } else { alert("Invalid attribute name: " + attribute_name); } } function set_token_bar_text_overlay(token_id, bar_num, option) { const select = $(`[data-tokenid=${token_id}] .bar${bar_num}options`); switch (option) { case "h": select.val("hidden"); break; case "e": select.val("editors"); break; case "*": select.val("everyone"); break; default: alert(`Invalid text overlay option: ${option}`); return; } } function set_token_aura_shape(token_id, aura_num, shape) { const select = $(`[data-tokenid=${token_id}] .aura${aura_num}_options`); switch (shape) { case "c": select.val("circle"); break; case "s": select.val("square"); break; default: alert(`Invalid aura shape: ${shape}`); return; } } function get_token_option_name(short_name) { const first = ({ "b": "Bar", "a": "Aura", "n": "Name", "t": "Tooltip", "*": "Global" })[short_name[0]]; const second = ({ 1: " 1", 2: " 2", 3: " 3", "*": " *", "v": " visibility", "e": " editability", "p": "plate", "l": " location", "s": " style", "t": " text" })[short_name[1]]; const third = ({ "v": " visibility", "e": " editability", "c": " current value", "m": " max value", "a": " linked attribute", "t": " text overlay visibility", "r": " radius", "s": " shape" })[short_name[2]] || ""; return first + second + third; }; function set_token_options(options) { const err_invalid_option = option => { alert(`Invalid option: ${option.name}=${option.value}`); }; options = options.split(',').map(option => { const [name, value] = option.split('=').map(o => o.trim()); return { name, value }; }); open_token_settings_async(id => { const get = sel => $("[data-tokenid="+id+"] "+sel); for (let i = 0; i < options.length; i++) { const option = options[i]; if (option.value === "?") { option.value = prompt(get_token_option_name(option.name)+":"); } const n = option.name[1]; switch (option.name) { case "b1c": // Bar current value case "b2c": case "b3c": { get(".bar"+n+"_value").val(option.value); break; } case "b*c": get(".bar1_value").val(option.value); get(".bar2_value").val(option.value); get(".bar3_value").val(option.value); break; case "b1m": // Bar max value case "b2m": case "b3m": get(".bar"+n+"_max").val(option.value); break; case "b*m": get(".bar1_max").val(option.value); get(".bar2_max").val(option.value); get(".bar3_max").val(option.value); break; case "b1a": // Bar linked attribute case "b2a": case "b3a": set_token_bar_attribute(id, n, option.value); break; case "b*a": set_token_bar_attribute(id, 1, option.value); set_token_bar_attribute(id, 2, option.value); set_token_bar_attribute(id, 3, option.value); break; case "b1v": // Bar view permission case "b2v": case "b3v": get(".showplayers_bar"+n).prop("checked", yn(option.value)); break; case "b*v": get(".showplayers_bar1").prop("checked", yn(option.value)); get(".showplayers_bar2").prop("checked", yn(option.value)); get(".showplayers_bar3").prop("checked", yn(option.value)); break; case "b1e": // Bar edit permissions case "b2e": case "b3e": get(".playersedit_bar"+n).prop("checked", yn(option.value)); break; case "b*e": get(".playersedit_bar1").prop("checked", yn(option.value)); get(".playersedit_bar2").prop("checked", yn(option.value)); get(".playersedit_bar3").prop("checked", yn(option.value)); break; case "b1t": // Bar text overlay visibility case "b2t": case "b3t": set_token_bar_text_overlay(id, n, option.value); break; case "b*t": set_token_bar_text_overlay(id, 1, option.value); set_token_bar_text_overlay(id, 2, option.value); set_token_bar_text_overlay(id, 3, option.value); break; case "bl": // Bar location switch (option.value) { case "a": get(".token_bar_location").val("above"); break; case "b": get(".token_bar_location").val("below"); break; case "ot": get(".token_bar_location").val("overlap_top"); break; case "ob": get(".token_bar_location").val("overlap_bottom"); break; default: err_invalid_option(option); return; } break; case "bs": // Bar style switch (option.value) { case "s": get("[name=barStyle][value=standard]").click(); break; case "c": get("[name=barStyle][value=compact]").click(); break; default: err_invalid_option(option); return; } break; case "a1r": case "a2r": get(".aura"+n+"_radius").val(option.value); break; case "a*r": get(".aura1_radius").val(option.value); get(".aura2_radius").val(option.value); break; case "a1s": case "a2s": set_token_aura_shape(id, n, option.value); break; case "a*s": set_token_aura_shape(id, 1, option.value); set_token_aura_shape(id, 2, option.value); break; case "a1v": // Aura view permission case "a2v": get(".showplayers_aura"+n).prop("checked", yn(option.value)); break; case "a*v": get(".showplayers_aura1").prop("checked", yn(option.value)); get(".showplayers_aura2").prop("checked", yn(option.value)); break; case "a1e": // Aura edit permissions case "a2e": get(".playersedit_aura"+n).prop("checked", yn(option.value)); break; case "a*e": get(".playersedit_aura1").prop("checked", yn(option.value)); get(".playersedit_aura2").prop("checked", yn(option.value)); break; case "nv": // Token name view permission get(".showplayers_name").prop("checked", yn(option.value)); break; case "ne": // Token name edit permission get(".playersedit_name").prop("checked", yn(option.value)); break; case "np": // Nameplate visibility get(".showname").prop("checked", yn(option.value)); break; case "nt": // Name text get(".name").val(option.value); break; case "tv": // Tooltip visible on hover get(".show_tooltip").prop("checked", yn(option.value)); break; case "tt": // Tooltip text get(".token-tooltip").val(option.value); break; case "*v": { // Global visibility permission const val = yn(option.value); get(".showplayers_bar1").prop("checked", val); get(".showplayers_bar2").prop("checked", val); get(".showplayers_bar3").prop("checked", val); get(".showplayers_aura1").prop("checked", val); get(".showplayers_aura2").prop("checked", val); get(".showplayers_name").prop("checked", val); break; } case "*e": { // Global editing permission const val = yn(option.value); get(".playersedit_bar1").prop("checked", val); get(".playersedit_bar2").prop("checked", val); get(".playersedit_bar3").prop("checked", val); get(".playersedit_aura1").prop("checked", val); get(".playersedit_aura2").prop("checked", val); get(".playersedit_name").prop("checked", val); break; } default: err_invalid_option(option); return; } } save_token_settings(id); }); } // Game/user preferences function set_preferences(prefs) { const err_invalid_pref = pref => { alert(`Invalid pref: ${pref.name}=${pref.value}`); }; prefs = prefs.split(',').map(pref => { const [name, value] = pref.split('=').map(o => o.trim()); return { name, value }; }); for (let i = 0; i < prefs.length; i++) { const pref = prefs[i]; const n = pref.name[1]; switch (pref.name) { case "aks": // Advanced keyboard shortcuts check_uncheck($("#checkboxAdvancedKeyboardShortcuts"), yn(pref.value)); break; case "wpc": // Window pop-outs for characters check_uncheck($("#checkboxCharacterPopoutWindows"), yn(pref.value)); break; case "bcb": // Background chat beep check_uncheck($("#checkboxBackgroundChatSounds"), yn(pref.value)); break; case "add": // Advanced dice check_uncheck($("#advancedDice"), yn(pref.value)); break; case "3dd": // 3D Dice check_uncheck($("#checkbox3dDice"), yn(pref.value)); break; case "a3d": // Automatically roll 3D dice check_uncheck($("#checkboxAutoRoll"), yn(pref.value)); break; case "cav": // Chat avatars check_uncheck($("#checkboxChatAvatars"), yn(pref.value)); break; case "cts": // Chat timestamps check_uncheck($("#checkboxChatTimestamps"), yn(pref.value)); break; case "sta": // Sort token actions (alphabetically) check_uncheck($("#checkboxSortTokenActionsByNameAsc"), yn(pref.value)); break; case "ani": // Animated graphics check_uncheck($("#checkboxAnimatedGraphics"), yn(pref.value)); break; case "ust": // Use scroll to... switch (pref.value) { case "z": // Zoom $("#useScrollTo").val("Zoom").change(); break; case "p": // Pan $("#useScrollTo").val("Pan").change(); break; default: err_invalid_pref(pref); break; } break; case "smp": // Status marker position switch (pref.value) { case "t": // Top $("#token_marker_position").val("top").change(); break; case "b": // Bottom $("#token_marker_position").val("bottom").change(); break; case "l": // Left $("#token_marker_position").val("left").change(); break; case "r": // Right $("#token_marker_position").val("right").change(); break; default: err_invalid_pref(pref); break; } break; case "pas": // Player avatar size switch (pref.value) { case "l": // Large $("#videoPlayerSize").val("large").change(); break; case "r": // Regular $("#videoPlayerSize").val("regular").change(); break; case "s": // Small $("#videoPlayerSize").val("small").change(); break; case "n": // Names only $("#videoPlayerSize").val("names").change(); break; default: err_invalid_pref(pref); break; } break; case "cht": { // Chat tech const select = $("option[value=roll20-fm]").parent(); switch (pref.value) { case "w": // WebRTC select.val("roll20-fm").change(); break; case "n": // None select.val("none").change(); break; default: err_invalid_pref(pref); break; } break; } default: err_invalid_pref(pref); break; } } } // Page controls function get_page(page_name) { return $(`.availablepage .page-title:contains(${page_name})`).parent(); } function go_to_page(players, gm, page_name) { if (!players && !gm) { console.warn("go_to_page() called with neither 'players' nor 'gm' set."); return; } const page = get_page(page_name); if (page.length < 1) { alert(`No page named '${page_name}' (remember, page names are case sensitive!)`); return; } // Open page toolbar $("#page-toolbar.closed .handle.showtip").click(); setTimeout(() => { // Change page for players if (players) { const bookmark = $(".playerbookmark"); const pageOffset = page.offset(); const bookmarkOffset = bookmark.offset(); bookmark.simulate("drag", { dx: pageOffset.left - bookmarkOffset.left, dy: pageOffset.top - bookmarkOffset.top }); } // Change page for GM if (gm) page.click(); // Close page toolbar $("#page-toolbar .handle").click(); }, 10); } function open_page_settings_async(callback) { $(".activepage .js__settings-page").click(); setTimeout(() => { const dialog = $(".ui-dialog-title:contains(Page Settings)").parents(".ui-dialog"); if (callback) { callback(dialog); } }, 10); } function set_page_settings(settings) { open_page_settings_async((dialog) => { const err_invalid_setting = s => { alert(`Invalid setting: ${s.name}=${s.value}`); }; settings = settings.split(',').map(s => { const [name, value] = s.split('=').map(o => o.trim()); return { name, value }; }); for (let i = 0; i < settings.length; i++) { const setting = settings[i]; const n = setting.name[1]; switch (setting.name) { case "dl": // Dynamic lighting dialog.find(".dyn_fog_enabled").prop("checked", yn(setting.value)); break; case "xm": // Explorer mode if (yn(setting.value)) { dialog.find(".dyn_fog_enabled").prop("checked", true); } dialog.find(".dyn_fog_autofog_mode").prop("checked", yn(setting.value)); break; case "gi": // Global illumination / daylight mode if (yn(setting.value)) { dialog.find(".dyn_fog_enabled").prop("checked", true); } dialog.find(".dyn_fog_global_illum").prop("checked", yn(setting.value)); break; case "ud": // Update on drop if (yn(setting.value)) { dialog.find(".dyn_fog_enabled").prop("checked", true); } dialog.find(".dyn_fog_update_on_drop").prop("checked", yn(setting.value)); break; case "rm": // Lighting restricts movement dialog.find(".lightrestrictmove").prop("checked", yn(setting.value)); break; case "fw": // Fog of war $("#page-standard-fog-basic-toggle").prop("checked", yn(setting.value)); break; case "ge": // Grid enabled $("#page-grid-display-toggle").prop("checked", yn(setting.value)); break; case "gs": // Grid scale $("#page-scale-grid-cell-distance").val(setting.value); break; case "gu": // Grid units switch (setting.value) { case "ft": case "m": case "km": case "mi": case "in": case "cm": case "un": case "hex": case "sq": case "custom": $("#page-scale-grid-cell-label-select").val(setting.value); break; default: alert(`Invalid grid scale unit: ${setting.value}`); break; } break; case "gt": // Grid type switch (setting.value) { case "s": // Square $("#gridtype").val("square"); break; case "v": // Square $("#gridtype").val("hex"); break; case "h": // Square $("#gridtype").val("hexr"); break; default: alert(`Invalid grid type: ${setting.value}`); break; } break; default: err_invalid_setting(setting); break; } } // Save dialog.find(".btn-primary").click(); }) } // Help function help() { $(help_html).appendTo("body") .find("button") .click((e) => { $(e.target).parent().detach(); }); } function create_table() { $("#addrollabletable").click(); setTimeout(() => { $(".rollabletable .name:contains(new-table)").last().click(); setTimeout(() => { const table_dialog = $(".ui-dialog-title:contains(new-table)").last().parents(".ui-dialog"); const table_name = prompt("Table name"); if (table_name) { table_dialog.find("input.name").val(table_name); } function get_item() { const item = prompt("Table item (blank to end)"); if (item) { table_dialog.find(".addtableitem").click(); setTimeout(() => { const item_dialog = $(".ui-dialog-title:contains(Edit Table Item)").last().parents(".ui-dialog"); item_dialog.find("input.name").val(item); item_dialog.find("button:contains(Save Changes)").click(); setTimeout(get_item, 10); }, 10); } else { table_dialog.find("button:contains(Save Changes)").click(); } } get_item(); }, 10); }, 500); } // Main function run_cmd(command) { const prefix = command[0]; const rest = command.slice(1); switch (prefix) { case "h": // Help help(); break; case "c": // Chat $("#textchat-input textarea").val(rest); $("#textchat-input button").click(); break; case "g": { // Page controls const subcmd = rest[0]; const subrest = rest.slice(1); switch (subcmd) { case "g": // Send GM to named page go_to_page(false, true, subrest); break; case "p": // Send players to named page go_to_page(true, false, subrest); break; case "a": // Send GM & players to named page go_to_page(true, true, subrest); break; case "s": // Open settings for current page set_page_settings(subrest); break; default: unknown_cmd(command); break; } break; } case "r": { // Roll dice roll_dice(rest); break; } case "a": { // Audio const subcmd = rest[0]; const subrest = rest.slice(1); switch (subcmd) { case "s": {// Stop all audio stop_audio_async(); break; } case "p": // Play playlist play_playlist(subrest); break; case "t": // Play track play_track(subrest); break; case "n": // Play next track next_track(); break; default: alert("Unknown audio subcommand: " + subcmd); break; } break; } case "n": // Create new things switch (rest[0]) { case "h": // Handout $("#addnewhandout").click(); break; case "c": // Character $("#addnewcharacter").click(); break; case "t": create_table(); break; default: unknown_cmd(command); break; } break; case "t": // Turn tracker switch (rest[0]) { case "o": // Next turn open_turn_tracker(); break; case "c": // Clear clear_turn_tracker(); break; case "x": // Close close_turn_tracker(); break; case "n": // Next turn $("button:contains(])").click(); break; case "s": // Sort $(".sortlist_numericdesc").click(); break; default: unknown_cmd(command); break; } break; case "v": // Vision switch (rest[0]) { case "n": // Normal open_token_settings_async((id) => { set_token_vision(id, true); set_token_dark_vision(id, 0); save_token_settings(id); }); break; case "x": // Blind open_token_settings_async((id) => { set_token_vision(id, false); save_token_settings(id); }); break; case "d": { // Darkvision const dist = rest.slice(1); if (isNaN(dist)) { alert("Invalid vision range: " + dist); } else { open_token_settings_async((id) => { set_token_dark_vision(id, Number(dist)); save_token_settings(id); }); } break; } default: unknown_cmd(command); break; } break; case "l": { // Lighting const match = rest.match(/(\d*)\/?(\d*)/); if (!match) { alert("Invalid light string: " + rest); } else { let [, bright, dim] = match; open_token_settings_async((id) => { set_token_bright_light(id, bright); set_token_dim_light(id, dim); save_token_settings(id); }); } break; } case "s": $("[data-action-type=show_marker_menu]").click(); setTimeout(() => { if (rest) { $("[data-action-type=toggle_status_"+rest+"]").click(); } else { $(".statusicon.active").click(); } $("[data-action-type=hide_marker_menu]").click(); }, 10); break; case "o": // Token options set_token_options(rest); break; case "p": // Preferences set_preferences(rest); break; case "m": // Macros switch (rest) { case "cs": // Start Combat open_turn_tracker(); clear_turn_tracker(); stop_audio_async(() => { play_playlist("Combat"); }); break; case "ce": // End Combat clear_turn_tracker(); close_turn_tracker(); stop_audio_async(() => { play_playlist("Dungeon"); }); break; default: unknown_cmd(command); break; } break; default: unknown_cmd(command); break; } } // Event handlers function text_keydown(e) { switch (e.key) { case "Enter": { e.stopPropagation(); e.preventDefault(); const command = e.target.value; e.target.value = ""; $(e.target).blur(); run_cmd(command); break; } case "Escape": { e.stopPropagation(); e.preventDefault(); e.target.value = ""; $(e.target).blur(); break; } } } function doc_keydown(e) { if (e.key === K_START) { e.preventDefault(); e.stopPropagation(); $(textBox).focus(); } } function dialog_trap_esc(e) { const focused = document.activeElement; if (e.key === "Escape" && $(focused).parents().is(".dialog")) { console.log("TRAPPED ESCAPE KEY TO AVOID LOSING DATA"); e.preventDefault(); e.stopPropagation(); } } // Initialize REKS $(style).appendTo("head"); $(textBox).appendTo("body"); textBox.get(0).addEventListener("keydown", text_keydown, false); document.addEventListener("keydown", doc_keydown, false); document.addEventListener("keydown", dialog_trap_esc, true); log("Roll20 Enhanced Keyboard Shortcuts initializeed."); }); })();