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 .
×
Create a free account

[Script] Real Rollable Tables

1418147758

Edited 1418153792
Lithl
Pro
Sheet Author
API Scripter
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(); });
1418152571
The Aaron
Roll20 Production Team
API Scripter
Nice dynamic dispatch, I think I might need to steal that... =D I see you're using a similar method of applying a version to the state that I use. I originally wrote the version of my script as the version for the data format in state. I switched to using a separate version (schemaVersion in my scripts) as I found I wanted to bump the version of the script without changing the format of the data in the state (and then wiping it out because it was different). In your case, generating the state again is non-destructive, so it doesn't matter. I store configuration data in state directly, so don't want to lose it. On the purpose side, I'm looking forward to trying this out. =D
1418153880
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: I see you're using a similar method of applying a version to the state that I use. I originally wrote the version of my script as the version for the data format in state. I switched to using a separate version (schemaVersion in my scripts) as I found I wanted to bump the version of the script without changing the format of the data in the state (and then wiping it out because it was different). In your case, generating the state again is non-destructive, so it doesn't matter. I store configuration data in state directly, so don't want to lose it. Yes, I stole your format because I liked it. =)
1418157754
The Aaron
Roll20 Production Team
API Scripter
Module Syntax, it's the awesome. I infected Stephen S. with it, too. =D It's nice because you can do cross-configuration in the on('ready',...) and be assured that everything is created at the time of use. Also lets me check for requirements (isGM being the primary one) and keeps the code nice and tidy. I'm a big fan of encapsulation. =D
1418169817
Falcon
Pro
Sheet Author
I just realized listening to you guys talk, how much of a Java Nooby I am. Like watching two architects talk...
1418171098

Edited 1418171175
The Aaron
Roll20 Production Team
API Scripter
There's this book I'd recommend... Pretty sure I already have though. :) Also, step 1 to not being a Jacascript Nooby is not calling it Java! :)
1418180905
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
The Aaron said: There's this book I'd recommend... Pretty sure I already have though. :) Also, step 1 to not being a Jacascript Nooby is not calling it Java! :) You ever notice how he recommends that book by Douglas Crockford.... AND ... he and Douglas Crockford are never in the same thread together. I'm not saying he is Douglas Crockford, I am just saying they never seem to post in the same thread together. Reach your own conclusion.
1418189715
The Aaron
Roll20 Production Team
API Scripter
Hahaha, we do both have impressive beards.. =D
Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author.
1418196965
Lithl
Pro
Sheet Author
API Scripter
manveti said: Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author. What does that say about my (female) scrum lead at work?
1418217047
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Brian said: manveti said: Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author. What does that say about my (female) scrum lead at work? I know! She is Dwarven.
1418219250

Edited 1510229555
The Aaron
Roll20 Production Team
API Scripter
manveti said: Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author. When I worked at Thompson Reuters on the Core Library team, we all had beards. It was such a contrast that management joked about not being able to hire anyone for the team that was clean shaven. If only they had known about this study. ;). I used to compare programmers to Samson: "the longer the hair, the stronger the programmer." Now I need to find that study...
1418219904

Edited 1510229566
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: manveti said: Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author. When I worked at Thompson Reuters on the Core Library team, we all had beards. It was such a contrast that management joked about not being able to hire anyone for the team that was clean shaven. If only they had known about this study. ;). I used to compare programmers to Samson: "the longer the hair, the stronger the programmer." Now I need to find that study... When I was in high school, I participated on the UIL CompSci team with four other guys, and all of us had long hair. The CS teacher who was our sponsor promised us to not cut his hair until we lost a competition. He was getting really worried that he might end up looking like a hippie when we went to Regionals. =)
1418226186
The Aaron
Roll20 Production Team
API Scripter
Brian said: The Aaron said: manveti said: Beards are important in the software world. Someone did a bit of statistical analysis and found that the strongest predictor of the success of a programming language is the beardliness of its author. When I worked at Thompson Reuters on the Core Livrary team, we all had beards. It was such a contrast that management joked about not being able to hire anyone for the team that was clean shaven. If only they had known about this study. ;). I used to compare programmers to Samson: "the longer the hair, the stronger the programmer." Now I need to find that study... When I was in high school, I participated on the UIL CompSci team with four other guys, and all of us had long hair. The CS teacher who was our sponsor promised us to not cut his hair until we lost a competition. He was getting really worried that he might end up looking like a hippie when we went to Regionals. =) Ha! I'd seen some of the pictures from that and wondered who that old hobo was... (hahaha only kidding!)
1418266400
Falcon
Pro
Sheet Author
Ok Ok... Brian - the script works like a charm. But I have all of these awesome scripts but none of them work together. What I mean by that is that I would love to use my PowerCard script while using this one. Is there ANYWAY we can run an API within an API? i KNOW THE ANSWER - it's no. But c'mon - there has to be a way...
Stuff that you send to the chat via sendChat does generate "chat:message" events just like any other chat, so if you want to you can assume (or check for) the existence of other scripts and issue chat commands to them. Alternatively, scripts are all dumped into the same context, so you can call functions from another script inside your script as long as they're designed to support that (or do so accidentally, which is probably more often the case). This way can be somewhat more powerful, but it's also somewhat more fragile unless you control both of the scripts in question or know that the owner of the script whose functions you're calling intends for them to be called directly by other scripts.
1418276419
The Aaron
Roll20 Production Team
API Scripter
Brian, there is an Off-By-One error. Here is a fix for it: 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 - 1 , items.length - 1), 0); } else { tableItemIndex = Math.max(Math.min(rollresult.total - 1 , items.length - 1), 0); }
1418278092
Falcon
Pro
Sheet Author
Ok - so in this case Powercards and this script (Powercards being the parent script) - what code would I need in my Powercards?
1418278696
Falcon
Pro
Sheet Author
Or if that can't be done can I have a smaller version of the script to put into the Powercard script with a new --??? If that is too much work then my other option is... can run a script where I roll and then put the result into an attribute that I can call in my Powercard script?
1418285119

Edited 1418285133
The Aaron
Roll20 Production Team
API Scripter
Hooking this script up to PowerCards would probably be pretty hard... You'd need to setup this script to preprocess a message, applying the roll to a table, then embed that roll in the original message as if had come from the Roll20 dice engine and pass it along to the power cards script... Ok. I did that. =D You can convert existing power commands by changing !power to !rtpower. You shouldn't see any difference when you do this, as by default it's just going to pass whatever arguments you give it on to the powercard script. !power --name|Test --Banana|[[1d12]] please! Becomes: !rtpower --name|Test --Banana|[[1d12]] please! [#[ Formula ]#] and @#{Character Name|Attribute} The real power is in passing some special formatted sections. I added 2: !rtpower --name|Test with Fromula --Use Fuel|[#[ 1d20+@#{bob|fuel} ]#] This first one provides formula evaluation at API execution time, instead of before it. You just change any formula that requires it from [[ Formula ]] to [#[ Formula ]#] and it will be handled by the API when it gets the message. This is particularly useful when you have macros with multiple API commands changing attributes because you can use @#{Character Name|Attribute Name|&lt;current|max&gt;} to cause the attribute to be looked up at API execution time. [rt:&lt;Table Name&gt;[ Formula ]#] This is the part you really want. It effectively does the !rt command, but as an inline roll passed to the power command. Just embed the table name and any formula supplied will be rolled and used as the offset into the table. The result of the roll is displayed in the expression by mousing over the roll. Here's an example with a table called d12min7 with the following weights: 7: 7 8: 1 9: 1 10: 1 11: 1 12: 1 !rtpower --name|Test --d12min7|[rt:d12min7[ 1d8+@{SomeStat} ]#] As you can see, I rolled a 4, which resulted in the value 7 from my table: Anyway, give it a try. There seems to be something a bit persnickety about the format the sendChat() will accept, so if it crashes, try getting rid of spaces in the formula portion. Labels also won't work there, but that shouldn't be a problem. GIST: <a href="https://gist.github.com/shdwjk/eda6313c93d51dbbfb8" rel="nofollow">https://gist.github.com/shdwjk/eda6313c93d51dbbfb8</a>...
1418328962
Falcon
Pro
Sheet Author
Amazing!!! So I don't need to change powercard macro - this above is both macros put together so I just need to put this one to create what you did in your example? the new command is !rtpower - right? If so - I will be testing this tonight when I get off work!!! Totally excited. I mean totally excited.
1418329199

Edited 1418329215
Falcon
Pro
Sheet Author
I don't think I explained how awesome this is well enough. FREAKING AWESOME!!! I can do things now that I couldn't do before - like crit tables based on damage done, fumble tables based on how bad they missed, out of control tables based on the speed and put them all in a powercard. Thank you Brian and Thank you Aaron!!!!!!!!
1418330140
The Aaron
Roll20 Production Team
API Scripter
No problem. =D I was gonna google chat you last night about it but I couldn't find your contact. =D (I need to rename it Black Falcon so I know who's who...) Let me know if you have any problems. I have some enhancements I want to do still (like making an inline roll to select which table to consult. That would let you handle your Star Frontiers multi column table by naming convention: !rtpower --result|[rt:sf[[@{selected|skill_level}]][1d100+@{selected|luck}]#] or what have you..
1418336798
Falcon
Pro
Sheet Author
I got it to work on one of my campaigns - I think I know why but need to test. Also - when it rolls the dice - it doesn't actually roll the 3D dice. It's not a big deal but interesting. Let me test why the other campaign doesn't work.
1418336940
Falcon
Pro
Sheet Author
You can't have both scripts (RT and RTPower) at the same time. That is why it didn't work.
1418343224
The Aaron
Roll20 Production Team
API Scripter
Ah. The script I posted is a modification to Brian's script, so if one was defined, the other would not be. I suppose I should just strip it out to be it's own thing...
1418402296
Falcon
Pro
Sheet Author
Its fine for me as I would only use the powercard version since my campaigns are based on that but if someone isn't using powercards then the original would be fine.
1418403899
The Aaron
Roll20 Production Team
API Scripter
If they are using the modified version I posted, they would still have the !rt command as my changes are purely additive (though my version has the off by one error fixed.).