Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

Rollable tables with modifier/advantage/as lookup

Score + 35
The ability to roll on a table using a modifier is something that has been asked about repeatedly for years. I propose being able to provide a roll table name as a roll/group modifier. That way we could use the full dice roller to handle tables. So for a d20 table with modifier, instead of creating duplicate tables for each possible modifier {d20 +?{Mod} }t[tablename] For rolling a table with advantage, instead of a duplicate table with advantage weightings 2d20k1t[tablename] As a lookup for custom messages, instead of the custom message trick involving a table with a single entry per message { message expression }t[tablename] With the full dice roller available this can be utilised in countless ways. If this interests you remember to vote by clicking the triangle next to the score at the top, below the thread title. Also feel free to tell us how you'd use it.
Cross suggestion potential: Custom dice syntax Having a custom dice syntax alongside being able to specify a table would allow granular control of rollable tables. To the point of being able to selectively enable/disable specific table entries but also handle multiple modified table rolls easily.
1682179844
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
This would be a godsend, and open up whole new vistas of macro functionality. I'm surprised it doesn't already have a thread.
1682238245

Edited 1684172856
GiGs
Pro
Sheet Author
API Scripter
Personally I'm pretty vocal about saying Rooll20's rollable tables are not fit for prupose and should be scrapped or renamed, and this is the number one reason why. I second this suggestion - tables need to support modifiers, or they aren't really tables in any meaningful sense.
I'll say, Rolltable modifiers would be quite handy in a lot of situations. I do wish I had them right now, it'd help me a lot with an issue I cannot solve without mods. And I'm still struggling to do so. A good ol' modifier would fix all my issues in a flash.
Yes this is very useful in a lot of situations. It's just one of those things that I wish Roll20 would bear down on and actually get  done.   R20 is a over a decade old and you still can't make a modified or open-ended die roll on a table.
I just stumbled across this old suggestion, and for Pro users I did want to point out that there is an Mod script for this, though it's not in the library. I can't remember who made it but someone showed it to me when I was trying to solve the same problem, and it works great. 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(); });