Inspired by Black Falcon's Suggestion, this script will allow you to roll on a rollable table with modifiers. This script will NOT roll multiple times on the same table (like 5t[table-name] will), nor will it interpret the values in the table in any way (if the result of the table is "5d8", the script will simply print that in chat, it won't roll 5 d8s).
Syntax
!rt [help|h] !rt table-name expression
If you call the !rt command with "help" or "h" as a parameter (or if the script fails to parse the parameters that you pass to it), the script will whisper the a block of help text to you, instructing you on the use of the command.
table-name must be the name of an existing table in the campaign, case-insensitive.
expression can be any dice expression supported by Roll20. (Note: errors in your dice expression will fail to generate any feedback, but will not crash the script.) If expression is a math-only roll, a single die of appropriate size will attempt to be used such that the possible rolls should stay on the table. (Sufficiently large values for expression or negative values for expression may force the results to clamp at the top or bottom of the table.)
If the result of expression would leave it hanging off the table in either direction, the result will be clamped onto the table. This means that at the extremes, the first and last element on the table have effectively higher weights.
Fractional weights on the table will be ignored. Currently, images on the table items are being ignored (only the name is printed to chat).
var RealRollableTable = RealRollableTable || (function() { 'use strict'; var version = 1, tables = _.groupBy(findObjs({ type: 'rollabletable' }), function(table) { return table.get('name').toLowerCase(); }), commands = { rt: function(args, msg) { var i, items, roll, table; if (args.length < 2) { commands.help_rt(args, msg); return; } table = tables[args[0].toLowerCase()]; if (!table) { table = findObjs({ type: 'rollabletable', name: args[0] }, { caseInsensitive: true })[0]; if (table) { tables[args[0].toLowerCase()] = table; } else { sendChat(getSystemFrom(), getWhisperTarget({ player: true, id: msg.playerid }) + 'Could not find table "' + args[0] + '". Please supply an existing table name.'); return; } } items = []; _.each(findObjs({ type: 'tableitem', rollabletableid: table.id }), function(element, index, list) { var i, weight = parseInt(element.get('weight')); for (i = 0; i < weight; i++) { items.push(element); } }); roll = _.rest(args).join(' '); if (msg.inlinerolls) { for (i = 0; i < msg.inlinerolls.length; i++) { roll = roll.replace('$[[' + i + ']]', msg.inlinerolls[i].results.total); } } sendChat('', '/r ' + roll, function(ops) { var rollresult = JSON.parse(ops[0].content), actualRoll = roll, displayTotal = rollresult.total, dieSize, tableItem, tableItemAvatar, tableItemIndex, tableItemName; if (rollresult.resultType === 'M') { // Math-only roll dieSize = Math.min(parseInt(items.length - rollresult.total), 0); actualRoll = '1d' + dieSize + ' + ' + roll; displayTotal = (dieSize > 0 ? randomInteger(dieSize) : 0) + rollresult.total; tableItemIndex = Math.max(Math.min(displayTotal, items.length - 1), 0); } else { tableItemIndex = Math.max(Math.min(rollresult.total, items.length - 1), 0); } tableItem = items[tableItemIndex]; tableItemAvatar = /*tableItem.get('avatar') ? '\n<img src="' + tableItem.get('avatar') + '" />' :*/ ''; //tableItemAvatar = tableItemAvatar.replace(/med|max/, 'thumb'); tableItemName = tableItem.get('name') ? '\n<span style="padding:3px;background-color:yellow"><b>' + tableItem.get('name') + '</b></span>' : ''; sendChat(getPlayerCharacterFrom(msg.who), 'Rolling ' + actualRoll + ' (' + displayTotal + ') on table "' + args[0] + '":' + tableItemAvatar + tableItemName); }); }, help: function(command, args, msg) { if (_.isFunction(commands['help_' + command])) { commands['help_' + command](args, msg); } }, help_rt: function(args, msg) { sendChat(getSystemFrom(), getWhisperTarget({ player: true, id: msg.playerid }) + '<div style="border:1px solid black;background:white;padding:3px 3px;">' + '<div style="font-weight:bold;border-bottom:1px solid black;font-size:130%;">' + 'Real Rollable Tables v' + state.RealRollableTable.version + '</div>' + '<span style="font-family:consolas"><b>!rt</b> <i>table-name roll</i></span>' + '<div style="padding-left:10px;margin-bottom:3px;">' + '<p>Supply a rolltable table\'s <span style="font-family:consolas">table-name</span> and a ' + '<span style="font-family:consolas">roll</span> to run on that table. If ' + '<span style="font-family:consolas">roll</span> is a math expression rather than a roll ' + 'expression, a single die of appropriate size will be selected such that rolling the maximum ' + 'value on the die will result in the last value in the table. If table items have weights ' + 'greater than 1, they will be treated as consecutive entries in the table (eg, a table with ' + 'items a:1, b:3, c:1, d:1 would be equivalent to a table with items a, b, b, b, c, d). ' + '<b>Fractional weights are ignored!</b></p>' + '<p>If the result of <span style="font-family:consolas">roll</span> would be lower than 1 or ' + 'greater than the number of elements in the table, the first or last element of the table will be ' + 'used as the result, as appropriate.</p>' + '</div>'); } }; function getPlayerCharacterFrom(name) { var character = findObjs({ type: 'character', name: name })[0], player = findObjs({ type: 'player', displayname: name.lastIndexOf(' (GM)') === name.length - 5 ? name.substring(0, name.length - 5) : name })[0]; if (player) { return 'player|' + player.id; } if (character) { return 'character|' + character.id; } return name; } function getWhisperTarget(options) { var nameProperty, targets, type; options = options || {}; if (options.player) { nameProperty = 'displayname'; type = 'player'; } else if (options.character) { nameProperty = 'name'; type = 'character'; } else { return ''; } if (options.id) { targets = [getObj(type, options.id)]; if (targets[0]) { return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' '; } } if (options.name) { targets = _.sortBy(filterObjs(function(obj) { if (obj.get('type') !== type) return false; return obj.get(nameProperty).indexOf(options.name) >= 0; }), function(obj) { return Math.abs(levenshteinDistance(obj.get(nameProperty), options.name)); }); if (targets[0]) { return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' '; } } return ''; } function splitArgs(input, separator) { var singleQuoteOpen = false, doubleQuoteOpen = false, tokenBuffer = [], ret = [], arr = input.split(''), element, i, matches; separator = separator || /\s/g; for (i = 0; i < arr.length; i++) { element = arr[i]; matches = element.match(separator); if (element === '\'') { if (!doubleQuoteOpen) { singleQuoteOpen = !singleQuoteOpen; continue; } } else if (element === '"') { if (!singleQuoteOpen) { doubleQuoteOpen = !doubleQuoteOpen; continue; } } if (!singleQuoteOpen && !doubleQuoteOpen) { if (matches) { if (tokenBuffer && tokenBuffer.length > 0) { ret.push(tokenBuffer.join('')); tokenBuffer = []; } } else { tokenBuffer.push(element); } } else if (singleQuoteOpen || doubleQuoteOpen) { tokenBuffer.push(element); } } if (tokenBuffer && tokenBuffer.length > 0) { ret.push(tokenBuffer.join('')); } return ret; } function levenshteinDistance(a, b) { var i, j, matrix = []; if (a.length === 0) { return b.length; } if (b.length === 0) { return a.length; } // Increment along the first column of each row for (i = 0; i <= b.length; i++) { matrix[i] = [i]; } // Increment each column in the first row for (j = 0; j <= a.length; j++) { matrix[0][j] = j; } // Fill in the rest of the matrix for (i = 1; i <= b.length; i++) { for (j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution Math.min(matrix[i][j - 1] + 1, // Insertion matrix[i - 1][j] + 1)); // Deletion } } } return matrix[b.length][a.length]; } function getSystemId() { // Get character id of `system` var systemChar = findObjs({ type: 'character', name: 'system', controlledby: '' }, { caseInsensitive: true })[0]; return systemChar ? systemChar.id : ''; } function getSystemFrom() { // Get appropriate value of `who` parameter for `sendChat` to send messages by System var systemChar, systemId; if (state.RealRollableTable.systemId !== '') { systemChar = getObj('character', state.RealRollableTable.systemId); } if (!systemChar) { systemId = getSystemId(); state.RealRollableTable.systemId = systemId; systemChar = getObj('character', state.RealRollableTable.systemId); } // If systemChar is undefined here, it doesn't exist return systemChar ? 'character|' + systemChar.id : 'System'; } function handleInput(msg) { var args, arg0, command, isApi, isHelp; isApi = msg.type === 'api'; args = splitArgs(msg.content.trim()); if (isApi) { // Call !command or help message for !command command = args.shift().substring(1).toLowerCase(); arg0 = args.shift(); if (arg0) { arg0 = arg0.toLowerCase(); } isHelp = arg0 === 'help' || arg0 === 'h'; if (!isHelp) { if (arg0 && arg0.length > 0) { args.unshift(arg0); } if (_.isFunction(commands[command])) { commands[command](args, msg); } } else if (_.isFunction(commands.help)) { commands.help(command, args, msg); } } else if (_.isFunction(commands['msg_' + msg.type])) { // Handle non-api command input commands['msg_' + msg.type](args, msg); } } function checkInstall() { // Initialize default `state` if (!state.RealRollableTable || !state.RealRollableTable.version || state.RealRollableTable.version !== version) { state.RealRollableTable = { version: version, systemId: getSystemId() }; } } function registerEventHandlers() { on('chat:message', handleInput); } return { checkInstall: checkInstall, registerEventHandlers: registerEventHandlers }; }()); on('ready', function() { 'use strict'; RealRollableTable.checkInstall(); RealRollableTable.registerEventHandlers(); });