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] FFG L5R Dice Roller

1507421467

Edited 1510419288
With the release of the Legend of the Five Rings RPG Beta by Fantasy Flight Games, the system, as many had expected, uses custom dice, but not the Genesys system that was expected by some of the community, but it's own hybrid between the mechanics seen in games like Edge of the Empire with some of L5R's Roll and Keep mechanism. Since I can only play online, and I use Roll20 as my primary means to do so, I decided to write up a quick script, aiming for the quality that the Edge of the Empire Dice Roller can provide.&nbsp; I'm nowhere near that level of quality, but I do have a working script that can do many basic rolling functionality, and has some primative rerolling logic.&nbsp; It doesn't provide the final results (since the system allows one to keep less than they are able), but I do eventually want to add some common result kept options given the dice roll, so people can more easily interpret the dice. Current Version: /** &nbsp;* Legend of the Five Rings 5e Dice Roller &nbsp;* @author DTemplar5 &nbsp;* @version 0.6 &nbsp;* Special thanks goes to the EotE dice developers (Konrad J.,Steve Day, Arron, Andrew H., Tom F., Akashan, GM Knowledge Rhino,Tim P.) for inspiration &nbsp;* Also thanks to Mike W. Leavitt for his Exalted dice roller was a place I could start from. &nbsp;* And thanks to you for being curious and looking at this!&nbsp; I've hid some easter eggs for you, care to find them? &nbsp;* New in Version 0.6: &nbsp;* -Added Kept dice command based on Max Success &nbsp;* -Added Compromised Flag to avoid keeping Strife-laden results &nbsp;* -Started Scorpion Clan Coup &nbsp;**/ /** &nbsp;* ToDo: &nbsp;*&nbsp; Continue working on expanding keep logic to include Max Advantage (with getting up to TN successes first), and Min Strife &nbsp;* -Add TN flag (-T or -t) to set the TN of the task; this also lets you know if you failed or succeeded &nbsp;* -Doesn't work on a secret TN setup though. &nbsp;* Continue Improving Results Display &nbsp;* -Formatting to look better and more inline with the dice &nbsp;* -Better dice images, just pulled from the beta PDF &nbsp;* -Pretty up the output &nbsp;* Scorpion Clan Coup &nbsp;**/ &nbsp; log('Loading L5R Dice Roller'); sendChat('L5R API', 'The Secrets of the Dice Roller can be found by typing &lt;code&gt;!l5r -help&lt;/code&gt;'); //Settings variable var display = { graphics: { ring : { blank : "<a href="https://i.imgur.com/rCOzYAB.png" rel="nofollow">https://i.imgur.com/rCOzYAB.png</a>", adv : "<a href="https://i.imgur.com/rONlOmw.png" rel="nofollow">https://i.imgur.com/rONlOmw.png</a>", advStrife : "<a href="https://i.imgur.com/VZwMdVG.png" rel="nofollow">https://i.imgur.com/VZwMdVG.png</a>", suc : "<a href="https://i.imgur.com/YR28QCA.png" rel="nofollow">https://i.imgur.com/YR28QCA.png</a>", sucStrife : "<a href="https://i.imgur.com/uFCPCR7.png" rel="nofollow">https://i.imgur.com/uFCPCR7.png</a>", sucXStrife : "<a href="https://i.imgur.com/tziXIv5.png" rel="nofollow">https://i.imgur.com/tziXIv5.png</a>" }, skill : { blank : "<a href="https://i.imgur.com/KGQK8Oa.png" rel="nofollow">https://i.imgur.com/KGQK8Oa.png</a>", adv : "<a href="https://i.imgur.com/ugYCuhM.png" rel="nofollow">https://i.imgur.com/ugYCuhM.png</a>", suc : "<a href="https://i.imgur.com/RgHzmr5.png" rel="nofollow">https://i.imgur.com/RgHzmr5.png</a>", sucStrife : "<a href="https://i.imgur.com/jk4Fhhb.png" rel="nofollow">https://i.imgur.com/jk4Fhhb.png</a>", sucXStrife : "<a href="https://i.imgur.com/NgBZyvz.png" rel="nofollow">https://i.imgur.com/NgBZyvz.png</a>", sucX : "<a href="https://i.imgur.com/MPudmqf.png" rel="nofollow">https://i.imgur.com/MPudmqf.png</a>", sucAdv : "<a href="https://i.imgur.com/KbeuQoj.png" rel="nofollow">https://i.imgur.com/KbeuQoj.png</a>" }, result : { suc : "<a href="https://i.imgur.com/NsXSIhS.png" rel="nofollow">https://i.imgur.com/NsXSIhS.png</a>", adv : "<a href="https://i.imgur.com/I0aoX2W.png" rel="nofollow">https://i.imgur.com/I0aoX2W.png</a>", strife : "<a href="https://i.imgur.com/aQ0UcQS.png" rel="nofollow">https://i.imgur.com/aQ0UcQS.png</a>" } }, size : { S : "30", M : "40", L : "50", X : "62", C : "30" } }; var graphics = true; var dice = { skill : [ display.graphics.skill.blank, display.graphics.skill.blank, display.graphics.skill.suc, display.graphics.skill.suc, display.graphics.skill.sucAdv, display.graphics.skill.sucStrife, display.graphics.skill.sucStrife, display.graphics.skill.sucX, display.graphics.skill.sucXStrife, display.graphics.skill.adv, display.graphics.skill.adv, display.graphics.skill.adv ], ring : [ display.graphics.ring.blank, display.graphics.ring.suc, display.graphics.ring.sucStrife, display.graphics.ring.sucXStrife, display.graphics.ring.adv, display.graphics.ring.advStrife ] }; /** &nbsp;* Main Body Loop for catching messages &nbsp;**/ on('chat:message', function(msg) { var apiWake = '!l5r '; if (msg.type == 'api' && msg.content.indexOf(apiWake) != -1) { //Splice the command do so some pattern matching to test it is working var slc = msg.content.slice(msg.content.indexOf(apiWake) + apiWake.length); var rawCmd = slc.trim(); var patt = /^.*\-[RrSsOoGg]\s[0-9]+/; if (patt.test(rawCmd)) { splitCommands(msg.content, msg.who); } else if (rawCmd.indexOf('-g') != -1 || rawCmd.indexOf('-G') != -1) { &nbsp; &nbsp; if(rawCmd.indexOf("on") != -1) { &nbsp; &nbsp; graphics = true; &nbsp; &nbsp; sendChat("L5RAPI", "The Graphics has been turned on"); &nbsp; &nbsp; } else { &nbsp; &nbsp; graphics = false; &nbsp; &nbsp; sendChat("L5RAPI", "The Graphics has been turned off"); &nbsp; &nbsp; } } else if (rawCmd.indexOf('-i s') != -1 || rawCmd.indexOf('-I s') != -1) { //Set graphics small display.size.C = display.size.S; sendChat("L5RAPI", "The Image Size is now Small (30x30)"); } else if (rawCmd.indexOf('-i m') != -1 || rawCmd.indexOf('-I m') != -1) { //Set graphics medium display.size.C = display.size.M; sendChat("L5RAPI", "The Image Size is now Medium (40x40)."); } else if (rawCmd.indexOf('-i l') != -1 || rawCmd.indexOf('-I l') != -1) { //Set graphics large display.size.C = display.size.L; sendChat("L5RAPI", "The Image Size is now Large (50x50)."); } else if (rawCmd.indexOf('-i x') != -1 || rawCmd.indexOf('-I x') != -1) { //Set graphics full size display.size.C = display.size.X; sendChat("L5RAPI", "The Image Size is now Maximum (62x62)."); } else if (rawCmd.indexOf('-help') != -1 || rawCmd.indexOf('-h') != -1) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var outHTML = buildHelp(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('EX3Dice API', '/w ' + msg.who + ' ' + outHTML); } else { &nbsp; &nbsp; printError(msg, msg.who); } } }); /** &nbsp;* splitCommands, parsing out each segment of the commands present and parsing them individually, rolling each dice segment and rerolling as needed &nbsp;* Not very clean, a bit bodgey, but uses RegEx to catch the first instance of each entry, &nbsp;* so doing -r 4 -r 5 will result in -r 4 getting rolled, and ignoring -r 5 completely. &nbsp;**/ function splitCommands(msg, caller) { var ringDice = 0; var skillDice = 0; var rerollDice = 0; var logicBlanks = false; var logicAdvantage = false; var logicStrife = false; var logicSuccess = false; var extraSuccess = 0; var extraAdvantage = 0; var extraStrife = 0; var compromised = false; var TN = 1; //If TN is not set, default to 1 for max advantage //Check if -R or -r is present for Ring Dice Rolling var ringPatt = /\-[Rr]\s([0-9]+)/; var ringRes = msg.match(ringPatt); log(ringRes); if(ringRes) ringDice = parseInt(ringRes[1]); //Check if -S or -s is present for Skill Dice Rolling var skillPatt = /\-[Ss]\s([0-9]+)/; var skillRes = msg.match(skillPatt); if(skillRes) skillDice = parseInt(skillRes[1]); //Check if -O or -o is present for Rerolling dice count var rerollPatt = /\-[Oo]\s([0-9]+)/; var rerollRes = msg.match(rerollPatt); if(rerollRes) rerollDice = parseInt(rerollRes[1]); //Check if -L or -l is present for Reroll Logic (b for blanks, a for advantage, x for strife, s for success) var logicPatt = /\-[Ll]\s([abxs]{1,4})/i; var logicRes = msg.match(logicPatt); if(logicRes) { logicBlanks = logicRes[1].indexOf('b') != -1; logicAdvantage = logicRes[1].indexOf('a') != -1; logicStrife = logicRes[1].indexOf('x') != -1; logicSuccess = logicRes[1].indexOf('s') != -1; } //Check if -A or -a is present for adding results (#s for successes, #a for advantage, and #x for strife) var extraPatt = /\-[Aa](\s[0-9]+[sax])+/gi; var extraRes = msg.match(extraPatt); //Currently not enabled if(extraRes) { //Need to use an exec loop instead of a match loop to parse each segment var extras = []; var xPatt = /\s[0-9]+[sax]/gi; var ematch; while((ematch = xPatt.exec(extraRes[0])) != null) { extras.push(ematch[0]); } for(var i = 0; i &lt; extras.length; ++i) { var extraSeg = extras[i].substring(1, extras[i].length -1); //Trim out the space and final character; if(extras[i].indexOf('a') != -1) { //Add Advantage extraAdvantage = parseInt(extraSeg); } else if(extras[i].indexOf('s') != -1) { //Add Success extraSuccess = parseInt(extraSeg); } else if(extras[i].indexOf('x') != -1) { //Add Strife extraStrife = parseInt(extraSeg); } } } //Check for the Compromised Flag var compPatt = /\-[Cc]/gi; var compRes = msg.match(compPatt); if(compRes) compromised = true; //Check if -T or -t is present for setting the TN var tnPatt = /\-[Tt]\s([0-9]+)/; var tnRes = msg.match(rerollPatt); if(tnRes) TN = parseInt(tnRes[1]); //Check if graphics has been changed (and if there is nothing else, exit cleanly) //It is NOT so you can throw lightswitch raves! var graphPatt = /\-[Gg]\s(on|off)/i; var graphRes = msg.match(graphPatt); if(graphRes) { if(graphRes[1] == "on") { graphics = true; } else { graphics = false; } if(ringDice + skillDice == 0) { //Exit cleanly if it is just a graphic toggle sendChat("L5RAPI", "The Graphics has been turned " + graphRes[1]); return; } } //Error check, if there's no Ring dice or Skill dice being rolled, error out //Note: This does allow Skill dice to be rolled without Ring dice if(ringDice + skillDice == 0) { printError(msg, msg.who); } //Pass the results over to the dice roller logic log("Extra Successes: " + extraSuccess); rollDice(ringDice, skillDice, rerollDice, logicBlanks, logicAdvantage, logicStrife, logicSuccess, extraSuccess, extraAdvantage, extraStrife, compromised, TN, caller); } /** &nbsp;* rollDice, the actual dice rolling with the commands parsed &nbsp;**/ function rollDice(ringDice, skillDice, rerollDice, logicBlanks, logicAdvantage, logicStrife, logicSuccess, extraSuccess, extraAdvantage, extraStrife, compromised, TN, caller) { var diceArray = []; var rerollArray = []; //Record of dice that were present and rerolled var kdlArray = []; //Record of dice currently used in pool for Kept Dice Logic /** * First, roll the skill dice, giving them priority over ring dice on what to reroll * 1 = (B)lank * 2 = (B)lank * 3 = (S)uccess * 4 = (S)uccess * 5 = (S)uccess and (A)dvantage * 6 = (S)uccess and (X) Strife * 7 = (S)uccess and (X) Strife * 8 = (S)uccess (exploding) * 9 = (S)uccess and (X)Strife (exploding) *10 = (A)dvantage *11 = (A)dvantage *12 = (A)dvantage **/ log("Skill Dice"); for(var i = 0; i &lt; skillDice; ++i) { var result = []; var tracker = []; //For conversion to results var d = 0; var reroll = false; //Don't reroll untill done do { reroll = false; //Reset reroll test in case rerolled do { d = randomInteger(12); tracker.push(d); switch(d) { case 1: //Fall thru case 2: result.push("B"); break; case 3: //Fall thru case 4: result.push("S"); break; case 5: result.push("SA"); break; case 6: //Fall thru case 7: result.push("SX"); break; case 8: result.push("S"); break; case 9: result.push("SX"); break; case 10: case 11: case 12: result.push("A"); break; default: log("ERROR! Bad result!"); throw "Error: Bad Result on Skill Roll."; } } while(d == 8 || d == 9); //Loop for explosions //Test for reroll, but for blanks, only reroll the blanks that are not part of the exploded dice, since reroll happens before dice are taken if((logicBlanks && rerollArray.length &lt; rerollDice && result[0] == "B") || (logicAdvantage && rerollArray.length &lt; rerollDice && result[0].indexOf("A") == 0) || (logicStrife && rerollArray.length &lt; rerollDice && result[0].indexOf("X") != -1) || (logicSuccess && rerollArray.length &lt; rerollDice && result[0].indexOf("S") != -1)) { reroll = true; if(graphics) { //If Graphics is enabled, convert the text to images, just worry about the first die, discard the rest of the results rerollArray.push([convertImage(tracker[0], "skill")]); } else { rerollArray.push(result); } result = []; //Clear the result for the reroll tracker = []; } } while(reroll); //Test for reroll //With final result, push into dice array and KDL Array if(graphics) { var input = []; var kdl = []; for(var j = 0; j &lt; tracker.length; ++j) { input.push(convertImage(tracker[j], "skill")); kdl.push([tracker[j], "skill"]); } kdlArray.push(kdl); diceArray.push(input); } else { var kdl = []; for(var j = 0; j &lt; tracker.length; ++j) { kdl.push([tracker[j], "skill"]); } kdlArray.push(kdl); diceArray.push(result); } result = []; tracker = []; } /** * Second, roll the ring dice; results as follows: * 1 = (B)lank * 2 = (S)uccess * 3 = (S)uccess and (X) Strife * 4 = (S)uccess and (X) Strife (exploding die) * 5 = (A)dvantage * 6 = (A)dvantage and (X) Strife **/ log("Ring Dice"); for(var i = 0; i &lt; ringDice; ++i) { var result = []; var tracker = []; //For conversion to results var d = 0; var reroll = false; //Don't reroll untill done do { reroll = false; //Reset reroll test in case rerolled do { d = randomInteger(6); tracker.push(d); switch(d) { case 1: result.push("B"); break; case 2: result.push("S"); break; case 3: //Fall thru case 4: result.push("SX"); break; case 5: result.push("A"); break; case 6: result.push("AX"); break; default: log("ERROR!&nbsp; Bad Result!"); throw "Error: Bad Result on Ring Dice"; } } while(d == 4); //Loop for explosions //Test for reroll, but for blanks, only reroll the blanks that are not part of the exploded dice, since reroll happens before dice are taken if((logicBlanks && rerollArray.length &lt; rerollDice && result[0] == "B") || (logicAdvantage && rerollArray.length &lt; rerollDice && result[0].indexOf("A") == 0) || (logicStrife && rerollArray.length &lt; rerollDice && result[0].indexOf("X") != -1) || (logicSuccess && rerollArray.length &lt; rerollDice && result[0].indexOf("S") != -1)) { reroll = true; if(graphics) { rerollArray.push([convertImage(tracker[0], "ring")]); } else { rerollArray.push(result); } result = []; //Clear the result for the reroll tracker = []; } } while(reroll); //Test for reroll //With final result, push into dice array if(graphics) { var input = []; var kdl = []; for(var j = 0; j &lt; tracker.length; ++j) { input.push(convertImage(tracker[j], "ring")); kdl.push([tracker[j], "ring"]); } kdlArray.push(kdl); diceArray.push(input); } else { var kdl = []; for(var j = 0; j &lt; tracker.length; ++j) { kdl.push([tracker[j], "ring"]); } kdlArray.push(kdl); diceArray.push(result); } result = []; tracker = []; } //Print Dice Result var diceString = getDiceString(diceArray, rerollArray, extraSuccess, extraAdvantage, extraStrife, compromised, TN, ringDice, skillDice, kdlArray, caller); sendChat('L5R API', diceString); } /** &nbsp;* printError, a simplified form of sending error messages &nbsp;**/ function printError(result, sender) { &nbsp; &nbsp; log('Error!'); &nbsp; &nbsp; if (result.type == 'error' ) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat('L5R API', '/w ' + sender + ' The peasents in charge of Roll20 could not obey.&nbsp; They said: ' + result.content); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; sendChat('L5R API', '/w ' + sender + ' The eta did not understand your command.&nbsp; Please try again.'); &nbsp; &nbsp; } } /** &nbsp;* getDiceString, turn the array values into readable text &nbsp;**/ function getDiceString(diceArray, rerollArray, extraSuccess, extraAdvantage, extraStrife, compromised, TN, ringDice, skillDice, kdlArray, caller) { //Get Count of dice var diceCount = ringDice + skillDice; /*for(var i = 0; i &lt; diceArray.length; ++i) { //Yes, inefficent, would be best to pass this from before diceCount += diceArray[i].length; }*/ //Deprecated!&nbsp; Woot! var returnString = "/direct &lt;table border=\"1\"&gt;&lt;tr&gt;&lt;th colspan=\"" + diceCount + "\"&gt;Results&lt;/th&gt;&lt;/tr&gt;&lt;td align=\"center\" colspan=\"" + diceCount + "\"&gt;" + caller + "&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;"; for(var i = 0; i &lt; diceArray.length; ++i) { var diceResult = "&lt;td&gt;"; //TODO: Put wrapper if length &gt; 1 for(var j = 0; j &lt; diceArray[i].length; ++j) { diceResult += diceArray[i][j]; } returnString += diceResult + "&lt;/td&gt;"; //TODO: Put closer if length &gt; 1 diceResult = ""; } returnString += "&lt;/tr&gt;"; if(rerollArray) { //Check if it exists first returnString += "&lt;tr&gt;&lt;td colspan=\"" + diceCount + "\"&gt;"; if(rerollArray.length &gt; 0) { returnString += "Rerolled Results: "; for(var i = 0; i &lt; rerollArray.length; ++i) { returnString += rerollArray[i] + " "; } } returnString += "&lt;/td&gt;&lt;/tr&gt;"; } if(extraSuccess &gt; 0 || extraAdvantage &gt; 0 || extraStrife &gt; 0) { //Add row for extra values added to the result returnString += "&lt;tr&gt;&lt;td colspan=\"" + diceCount + "\"&gt;Added Results: "; log(returnString.length); var j = 0; //Iterator if(graphics) { &nbsp; &nbsp; while(j &lt; extraSuccess) { //Skip if over 0 &nbsp; &nbsp; returnString += imageString(display.graphics.result.suc); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } &nbsp; &nbsp; j = 0; &nbsp; &nbsp; while(j &lt; extraAdvantage) { &nbsp; &nbsp; returnString += imageString(display.graphics.result.adv); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } &nbsp; &nbsp; j = 0; &nbsp; &nbsp; while(j &lt; extraStrife) { &nbsp; &nbsp; returnString += imageString(display.graphics.result.strife); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } } else { &nbsp; &nbsp; if(extraSuccess &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraSuccess + "S "; &nbsp; &nbsp; } &nbsp; &nbsp; if(extraAdvantage &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraAdvantage + "A "; &nbsp; &nbsp; } &nbsp; &nbsp; if(extraStrife &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraStrife + "X"; &nbsp; &nbsp; } } returnString += "&lt;/td&gt;&lt;/tr&gt;"; } //Add the kept dice segment of the logic here returnString += keptDice(kdlArray, ringDice, compromised, TN); //Still keep dice based on ring: Void Points effective are +1 to ring returnString += "&lt;/table&gt;"; return returnString; } /** &nbsp;* imageString, helper function to build the HTML to display the dice result &nbsp;**/ function imageString(imgStr) { return "&lt;img src=\"" + imgStr + "\" height=\"" + display.size.C + "\" width=\"" + display.size.C + "\" /&gt;"; } /** &nbsp;* convertImage, helper function to convert a text result to the image file &nbsp;**/ function convertImage(dieNumber, die) { if(die == "skill") { return imageString(dice.skill[dieNumber-1]); } else { //ring return imageString(dice.ring[dieNumber-1]); } log('Error'); throw "Error: conversion to text failed"; } /** &nbsp;*buildHelp, the function to return Help formatted in an easy to understand fashion &nbsp;*ToDo: Revise help to be pretty looking &nbsp;**/ function buildHelp() { var helpString = ""; helpString = "&lt;code&gt;!l5r [cmd] [cmdoption] [cmd] [cmdoption]...&lt;/code&gt;&lt;br&gt;"; helpString += "-r (number) or -R (number): Roll (number) of Ring Dice&lt;br&gt;"; helpString += "-s (number) or -S (number): Roll (number) of Skill Dice&lt;br&gt;"; helpString += "-o (number) or -O (number): Reroll (number) of dice (Letter o/O, NOT 0)&lt;br&gt;"; helpString += "-l (s|a|b|x) or -L (s|a|b|x): Flags for reroll logic, reroll on (a)dvantage, (b)lanks, (s)uccess and/or (x) strife.&lt;br&gt;"; helpString += "-a ((number)a | (number)s | (number)x) or -A ((number)a | (number)s | (number)x): Additions to final result (a)dvantage, (s)uccess, and (x) strife.&lt;br&gt;"; helpString += "EXAMPLE: To add 1 advantage and 1 success, this would be done as -a 1a 1s not -a 1a -a 1s&lt;br&gt;"; helpString += "-g (on|off) or -G (on|off): Enable or disable graphic images (defaults to on)&lt;br&gt;" helpString += "-i (s|m|l|x) or -I (s|m|l|x): Flags for setting the size of images (defaults to s)&lt;br&gt;"; helpString += "-c: Compromised status flag (Prevent keep logic from keeping strife results)&lt;br&gt;"; return helpString; } /** &nbsp;*keptDice, the function to generate logic based on which dice are kept or not &nbsp;*Returns a string to add to the results &nbsp;*/ function keptDice(diceArray, keptDice, compromised, TN) { var maxSuccess = ""; var maxAdv = ""; var minStrife = ""; log('The Kept Dice Function: Dice Array'); log(diceArray); var successArray = _.sortBy(diceArray, function(die) { &nbsp; &nbsp; //log(die); if(die[0][1] == "skill") { switch(die[0][0]) { case 1: return 0; case 2: return 0; case 3: return -4; case 4: return -4; case 5: return -5; case 6: return -3; case 7: return -3; case 8: return -7; case 9: return -6; case 10: return -2; case 11: return -2; case 12: return -2; default: return 9001; } } else { //ring switch(die[0][0]) { case 1: return 0; case 2: return -4; case 3: return -3; case 4: return -6; case 5: return -2; case 6: return -1; default: return 9001; } } }); if(compromised) { successArray = _.reject(successArray, function(die) { if(die[0][1] == "skill") { switch(die[0][0]) { case 9: return true; case 6: return true; case 7: return true; default: return false; } } else { //ring switch(die[0][0]) { case 4: return true; case 3: return true; case 6: return true; default: return false; } } }); _.each(successArray, function(die) { &nbsp; &nbsp; for(var i = 0; i &lt; die.length; ++i) { &nbsp; &nbsp; &nbsp; &nbsp; switch(die[i][0]) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 6: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 7: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 9: die.splice(i, 1); break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: break; //Do nothing &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } }); if(successArray.length &gt; keptDice) { //If too big after compromised check, trim down to first keptDice successArray = _.first(successArray, keptDice); } } else { //Just trim the first keptDice successArray = _.first(successArray, keptDice); } log(successArray); maxSuccess = "&lt;tr&gt;&lt;td align=\"center\" colspan=\"" + diceArray.length + "\"&gt;Max Success Results&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;"; _.each(successArray, function(die) { &nbsp; &nbsp; maxSuccess += "&lt;td&gt;"; &nbsp; &nbsp; for(var x = 0; x &lt; die.length; ++x) { &nbsp; &nbsp; &nbsp; &nbsp; maxSuccess += convertImage(die[x][0], die[x][1]); &nbsp; &nbsp; } &nbsp; &nbsp; maxSuccess += "&lt;/td&gt;"; }); for(var x = successArray.length; x &lt; diceArray.length; ++x) { &nbsp; &nbsp; maxSuccess += "&lt;td&gt; &lt;/td&gt;"; //Padding } maxSuccess += "&lt;/tr&gt;"; return maxSuccess; } Old Version (keeping for now): /** &nbsp;* Legend of the Five Rings 5e Dice Roller &nbsp;* @author DTemplar5 &nbsp;* @version 0.5 &nbsp;* Special thanks goes to the EotE dice developers (Konrad J.,Steve Day, Arron, Andrew H., Tom F., Akashan, GM Knowledge Rhino,Tim P.) for inspiration &nbsp;* Also thanks to Mike W. Leavitt for his Exalted dice roller was a place I could start from. &nbsp;* And thanks to you for being curious and looking at this!&nbsp; I've hid some easter eggs for you, care to find them? &nbsp;**/ /** &nbsp;* ToDo: &nbsp;* Improving Results Display &nbsp;* -Formatting to look better and more inline with the dice &nbsp;* -Showing different Kept Results (Max Success, Max Advantage, Min Strife) &nbsp;* -Better dice images, just pulled from the beta PDF &nbsp;* Scorpion Clan Coup &nbsp;**/ &nbsp; log('Loading L5R Dice Roller'); sendChat('L5R API', 'The Secrets of the Dice Roller can be found by typing &lt;code&gt;!l5r -help&lt;/code&gt;'); //Settings variable var display = { graphics: { ring : { blank : "<a href="https://i.imgur.com/rCOzYAB.png" rel="nofollow">https://i.imgur.com/rCOzYAB.png</a>", adv : "<a href="https://i.imgur.com/rONlOmw.png" rel="nofollow">https://i.imgur.com/rONlOmw.png</a>", advStrife : "<a href="https://i.imgur.com/VZwMdVG.png" rel="nofollow">https://i.imgur.com/VZwMdVG.png</a>", suc : "<a href="https://i.imgur.com/YR28QCA.png" rel="nofollow">https://i.imgur.com/YR28QCA.png</a>", sucStrife : "<a href="https://i.imgur.com/uFCPCR7.png" rel="nofollow">https://i.imgur.com/uFCPCR7.png</a>", sucXStrife : "<a href="https://i.imgur.com/tziXIv5.png" rel="nofollow">https://i.imgur.com/tziXIv5.png</a>" }, skill : { blank : "<a href="https://i.imgur.com/KGQK8Oa.png" rel="nofollow">https://i.imgur.com/KGQK8Oa.png</a>", adv : "<a href="https://i.imgur.com/ugYCuhM.png" rel="nofollow">https://i.imgur.com/ugYCuhM.png</a>", suc : "<a href="https://i.imgur.com/RgHzmr5.png" rel="nofollow">https://i.imgur.com/RgHzmr5.png</a>", sucStrife : "<a href="https://i.imgur.com/jk4Fhhb.png" rel="nofollow">https://i.imgur.com/jk4Fhhb.png</a>", sucXStrife : "<a href="https://i.imgur.com/NgBZyvz.png" rel="nofollow">https://i.imgur.com/NgBZyvz.png</a>", sucX : "<a href="https://i.imgur.com/MPudmqf.png" rel="nofollow">https://i.imgur.com/MPudmqf.png</a>", sucAdv : "<a href="https://i.imgur.com/KbeuQoj.png" rel="nofollow">https://i.imgur.com/KbeuQoj.png</a>" }, result : { suc : "<a href="https://i.imgur.com/NsXSIhS.png" rel="nofollow">https://i.imgur.com/NsXSIhS.png</a>", adv : "<a href="https://i.imgur.com/I0aoX2W.png" rel="nofollow">https://i.imgur.com/I0aoX2W.png</a>", strife : "<a href="https://i.imgur.com/aQ0UcQS.png" rel="nofollow">https://i.imgur.com/aQ0UcQS.png</a>" } }, size : { S : "30", M : "40", L : "50", X : "62", C : "30" } }; var graphics = true; var dice = { skill : [ display.graphics.skill.blank, display.graphics.skill.blank, display.graphics.skill.suc, display.graphics.skill.suc, display.graphics.skill.sucAdv, display.graphics.skill.sucStrife, display.graphics.skill.sucStrife, display.graphics.skill.sucX, display.graphics.skill.sucXStrife, display.graphics.skill.adv, display.graphics.skill.adv, display.graphics.skill.adv ], ring : [ display.graphics.ring.blank, display.graphics.ring.suc, display.graphics.ring.sucStrife, display.graphics.ring.sucXStrife, display.graphics.ring.adv, display.graphics.ring.advStrife ] }; /** &nbsp;* Main Body Loop for catching messages &nbsp;**/ on('chat:message', function(msg) { var apiWake = '!l5r '; if (msg.type == 'api' && msg.content.indexOf(apiWake) != -1) { //Splice the command do so some pattern matching to test it is working var slc = msg.content.slice(msg.content.indexOf(apiWake) + apiWake.length); var rawCmd = slc.trim(); var patt = /^.*\-[RrSsOoGg]\s[0-9]+/; if (patt.test(rawCmd)) { splitCommands(msg.content, msg.who); } else if (rawCmd.indexOf('-g') != -1 || rawCmd.indexOf('-G') != -1) { &nbsp; &nbsp; if(rawCmd.indexOf("on") != -1) { &nbsp; &nbsp; graphics = true; &nbsp; &nbsp; sendChat("L5RAPI", "The Graphics has been turned on"); &nbsp; &nbsp; } else { &nbsp; &nbsp; graphics = false; &nbsp; &nbsp; sendChat("L5RAPI", "The Graphics has been turned off"); &nbsp; &nbsp; } } else if (rawCmd.indexOf('-i s') != -1 || rawCmd.indexOf('-I s') != -1) { //Set graphics small display.size.C = display.size.S; sendChat("L5RAPI", "The Image Size is now Small (30x30)"); } else if (rawCmd.indexOf('-i m') != -1 || rawCmd.indexOf('-I m') != -1) { //Set graphics medium display.size.C = display.size.M; sendChat("L5RAPI", "The Image Size is now Medium (40x40)."); } else if (rawCmd.indexOf('-i l') != -1 || rawCmd.indexOf('-I l') != -1) { //Set graphics large display.size.C = display.size.L; sendChat("L5RAPI", "The Image Size is now Large (50x50)."); } else if (rawCmd.indexOf('-i x') != -1 || rawCmd.indexOf('-I x') != -1) { //Set graphics full size display.size.C = display.size.X; sendChat("L5RAPI", "The Image Size is now Maximum (62x62)."); } else if (rawCmd.indexOf('-help') != -1 || rawCmd.indexOf('-h') != -1) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var outHTML = buildHelp(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('EX3Dice API', '/w ' + msg.who + ' ' + outHTML); } else { &nbsp; &nbsp; printError(msg, msg.who); } } }); /** &nbsp;* splitCommands, parsing out each segment of the commands present and parsing them individually, rolling each dice segment and rerolling as needed &nbsp;* Not very clean, a bit bodgey, but uses RegEx to catch the first instance of each entry, &nbsp;* so doing -r 4 -r 5 will result in -r 4 getting rolled, and ignoring -r 5 completely. &nbsp;**/ function splitCommands(msg, caller) { var ringDice = 0; var skillDice = 0; var rerollDice = 0; var logicBlanks = false; var logicAdvantage = false; var logicStrife = false; var logicSuccess = false; var extraSuccess = 0; var extraAdvantage = 0; var extraStrife = 0; //Check if -R or -r is present for Ring Dice Rolling var ringPatt = /\-[Rr]\s([0-9]+)/; var ringRes = msg.match(ringPatt); if(ringRes) ringDice = parseInt(ringRes[1]); //Check if -S or -s is present for Skill Dice Rolling var skillPatt = /\-[Ss]\s([0-9]+)/; var skillRes = msg.match(skillPatt); if(skillRes) skillDice = parseInt(skillRes[1]); //Check if -O or -o is present for Rerolling dice count var rerollPatt = /\-[Oo]\s([0-9]+)/; var rerollRes = msg.match(rerollPatt); if(rerollRes) rerollDice = parseInt(rerollRes[1]); //Check if -L or -l is present for Reroll Logic (b for blanks, a for advantage, x for strife, s for success) var logicPatt = /\-[Ll]\s([abxs]{1,4})/i; var logicRes = msg.match(logicPatt); if(logicRes) { logicBlanks = logicRes[1].indexOf('b') != -1; logicAdvantage = logicRes[1].indexOf('a') != -1; logicStrife = logicRes[1].indexOf('x') != -1; logicSuccess = logicRes[1].indexOf('s') != -1; } //Check if -A or -a is present for adding results (#s for successes, #a for advantage, and #x for strife) var extraPatt = /\-[Aa](\s[0-9]+[sax])+/i; var extraRes = msg.match(extraPatt); //Currently not enabled if(extraRes) { //Need to use an exec loop instead of a match loop to parse each segment var extras = []; var xPatt = /\s[0-9]+[sax]/gi; var ematch; while((ematch = xPatt.exec(extraRes[0])) != null) { extras.push(ematch[0]); } for(var i = 0; i &lt; extras.length; ++i) { var extraSeg = extras[i].substring(1, extras[i].length -1); //Trim out the space and final character; if(extras[i].indexOf('a') != -1) { //Add Advantage extraAdvantage = parseInt(extraSeg); } else if(extras[i].indexOf('s') != -1) { //Add Success extraSuccess = parseInt(extraSeg); } else if(extras[i].indexOf('x') != -1) { //Add Strife extraStrife = parseInt(extraSeg); } } //log("Success: " + extraSuccess + " Advantage: " + extraAdvantage + " Strife: " + extraStrife); } //Check if graphics has been changed (and if there is nothing else, exit cleanly) //It is NOT so you can throw lightswitch raves! var graphPatt = /\-[Gg]\s(on|off)/i; var graphRes = msg.match(graphPatt); if(graphRes) { if(graphRes[1] == "on") { graphics = true; } else { graphics = false; } if(ringDice + skillDice == 0) { //Exit cleanly if it is just a graphic toggle sendChat("L5RAPI", "The Graphics has been turned " + graphRes[1]); return; } } //Error check, if there's no Ring dice or Skill dice being rolled, error out //Note: This does allow Skill dice to be rolled without Ring dice if(ringDice + skillDice == 0) { printError(msg, msg.who); } //Pass the results over to the dice roller logic rollDice(ringDice, skillDice, rerollDice, logicBlanks, logicAdvantage, logicStrife, logicSuccess, extraSuccess, extraAdvantage, extraStrife, caller); } /** &nbsp;* rollDice, the actual dice rolling with the commands parsed &nbsp;**/ function rollDice(ringDice, skillDice, rerollDice, logicBlanks, logicAdvantage, logicStrife, logicSuccess, extraSuccess, extraAdvantage, extraStrife, caller) { var diceArray = []; var rerollArray = []; //Record of dice that were present and rerolled /** * First, roll the skill dice, giving them priority over ring dice on what to reroll * 1 = (B)lank * 2 = (B)lank * 3 = (S)uccess * 4 = (S)uccess * 5 = (S)uccess and (A)dvantage * 6 = (S)uccess and (X) Strife * 7 = (S)uccess and (X) Strife * 8 = (S)uccess (exploding) * 9 = (S)uccess and (X)Strife (exploding) *10 = (A)dvantage *11 = (A)dvantage *12 = (A)dvantage **/ for(var i = 0; i &lt; skillDice; ++i) { var result = []; var tracker = []; //For conversion to results var d = 0; var reroll = false; //Don't reroll untill done do { reroll = false; //Reset reroll test in case rerolled do { d = randomInteger(12); tracker.push(d); switch(d) { case 1: //Fall thru case 2: result.push("B"); break; case 3: //Fall thru case 4: result.push("S"); break; case 5: result.push("SA"); break; case 6: //Fall thru case 7: result.push("SX"); break; case 8: result.push("S"); break; case 9: result.push("SX"); break; case 10: case 11: case 12: result.push("A"); break; default: log("ERROR! Bad result!"); throw "Error: Bad Result on Skill Roll."; } } while(d == 8 || d == 9); //Loop for explosions //Test for reroll, but for blanks, only reroll the blanks that are not part of the exploded dice, since reroll happens before dice are taken if((logicBlanks && rerollArray.length &lt; rerollDice && result[0] == "B") || (logicAdvantage && rerollArray.length &lt; rerollDice && result[0].indexOf("A") == 0) || (logicStrife && rerollArray.length &lt; rerollDice && result[0].indexOf("X") != -1) || (logicSuccess && rerollArray.length &lt; rerollDice && result[0].indexOf("S") != -1)) { reroll = true; if(graphics) { //If Graphics is enabled, convert the text to images, just worry about the first die, discard the rest of the results rerollArray.push([convertImage(tracker[0], "skill")]); } else { rerollArray.push(result); } result = []; //Clear the result for the reroll tracker = []; } } while(reroll); //Test for reroll //With final result, push into dice array if(graphics) { var input = []; for(var j = 0; j &lt; tracker.length; ++j) { input.push(convertImage(tracker[j], "skill")); } diceArray.push(input); } else { diceArray.push(result); } result = []; tracker = []; } /** * Second, roll the ring dice; results as follows: * 1 = (B)lank * 2 = (S)uccess * 3 = (S)uccess and (X) Strife * 4 = (S)uccess and (X) Strife (exploding die) * 5 = (A)dvantage * 6 = (A)dvantage and (X) Strife **/ for(var i = 0; i &lt; ringDice; ++i) { var result = []; var tracker = []; //For conversion to results var d = 0; var reroll = false; //Don't reroll untill done do { reroll = false; //Reset reroll test in case rerolled do { d = randomInteger(6); tracker.push(d); switch(d) { case 1: result.push("B"); break; case 2: result.push("S"); break; case 3: //Fall thru case 4: result.push("SX"); break; case 5: result.push("A"); break; case 6: result.push("AX"); break; default: log("ERROR!&nbsp; Bad Result!"); throw "Error: Bad Result on Ring Dice"; } } while(d == 4); //Loop for explosions //Test for reroll, but for blanks, only reroll the blanks that are not part of the exploded dice, since reroll happens before dice are taken if((logicBlanks && rerollArray.length &lt; rerollDice && result[0] == "B") || (logicAdvantage && rerollArray.length &lt; rerollDice && result[0].indexOf("A") == 0) || (logicStrife && rerollArray.length &lt; rerollDice && result[0].indexOf("X") != -1) || (logicSuccess && rerollArray.length &lt; rerollDice && result[0].indexOf("S") != -1)) { reroll = true; if(graphics) { rerollArray.push([convertImage(tracker[0], "ring")]); } else { rerollArray.push(result); } result = []; //Clear the result for the reroll tracker = []; } } while(reroll); //Test for reroll //With final result, push into dice array if(graphics) { var input = []; for(var j = 0; j &lt; tracker.length; ++j) { input.push(convertImage(tracker[j], "ring")); } diceArray.push(input); } else { diceArray.push(result); } result = []; tracker = []; } //Print Dice Result var diceString = getDiceString(diceArray, rerollArray, extraSuccess, extraAdvantage, extraStrife, caller); sendChat('L5R API', diceString); } /** &nbsp;* printError, a simplified form of sending error messages &nbsp;**/ function printError(result, sender) { &nbsp; &nbsp; log('Error!'); &nbsp; &nbsp; if (result.type == 'error' ) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat('L5R API', '/w ' + sender + ' The peasents in charge of Roll20 could not obey.&nbsp; They said: ' + result.content); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; sendChat('L5R API', '/w ' + sender + ' The eta did not understand your command.&nbsp; Please try again.'); &nbsp; &nbsp; } } /** &nbsp;* getDiceString, turn the array values into readable text &nbsp;**/ function getDiceString(diceArray, rerollArray, extraSuccess, extraAdvantage, extraStrife, caller) { //Get Count of dice var diceCount = 0; for(var i = 0; i &lt; diceArray.length; ++i) { //Yes, inefficent, would be best to pass this from before diceCount += diceArray[i].length; } var returnString = "/direct &lt;table border=\"1\"&gt;&lt;tr&gt;&lt;th colspan=\"" + diceCount + "\"&gt;Results&lt;/th&gt;&lt;/tr&gt;&lt;td colspan=\"" + diceCount + "\"&gt;" + caller + "&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;"; for(var i = 0; i &lt; diceArray.length; ++i) { var diceResult = "&lt;td&gt;"; //TODO: Put wrapper if length &gt; 1 for(var j = 0; j &lt; diceArray[i].length; ++j) { diceResult += diceArray[i][j]; } returnString += diceResult + "&lt;/td&gt;"; //TODO: Put closer if length &gt; 1 diceResult = ""; } returnString += "&lt;/tr&gt;"; if(rerollArray) { //Check if it exists first returnString += "&lt;tr&gt;&lt;td colspan=\"" + diceCount + "\"&gt;"; if(rerollArray.length &gt; 0) { returnString += "Rerolled Results: "; for(var i = 0; i &lt; rerollArray.length; ++i) { returnString += rerollArray[i] + " "; } } returnString += "&lt;/td&gt;&lt;/tr&gt;"; } if(extraSuccess &gt; 0 || extraAdvantage &gt; 0 || extraStrife &gt; 0) { //Add row for extra values added to the result returnString += "&lt;tr&gt;&lt;td colspan=\"" + diceCount + "\"&gt;Added Results: "; log(returnString.length); var j = 0; //Iterator if(graphics) { &nbsp; &nbsp; while(j &lt; extraSuccess) { //Skip if over 0 &nbsp; &nbsp; returnString += imageString(display.graphics.result.suc); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } &nbsp; &nbsp; j = 0; &nbsp; &nbsp; while(j &lt; extraAdvantage) { &nbsp; &nbsp; returnString += imageString(display.graphics.result.adv); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } &nbsp; &nbsp; j = 0; &nbsp; &nbsp; while(j &lt; extraStrife) { &nbsp; &nbsp; returnString += imageString(display.graphics.result.strife); &nbsp; &nbsp; ++j; &nbsp; &nbsp; } } else { &nbsp; &nbsp; if(extraSuccess &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraSuccess + "S "; &nbsp; &nbsp; } &nbsp; &nbsp; if(extraAdvantage &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraAdvantage + "A "; &nbsp; &nbsp; } &nbsp; &nbsp; if(extraStrife &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; returnString += extraStrife + "X"; &nbsp; &nbsp; } } returnString += "&lt;/td&gt;&lt;/tr&gt;"; } returnString += "&lt;/table&gt;"; return returnString; } /** &nbsp;* imageString, helper function to build the HTML to display the dice result &nbsp;**/ function imageString(imgStr) { return "&lt;img src=\"" + imgStr + "\" height=\"" + display.size.C + "\" width=\"" + display.size.C + "\" /&gt;"; } /** &nbsp;* convertImage, helper function to convert a text result to the image file &nbsp;**/ function convertImage(dieNumber, die) { if(die == "skill") { return imageString(dice.skill[dieNumber-1]); } else { //ring return imageString(dice.ring[dieNumber-1]); } log('Error'); throw "Error: conversion to text failed"; } /** &nbsp;* buildHelp, the function to return Help formatted in an easy to understand fashion &nbsp;* ToDo: Revise help to be pretty looking &nbsp;**/ function buildHelp() { var helpString = ""; helpString = "&lt;code&gt;!l5r [cmd] [cmdoption] [cmd] [cmdoption]...&lt;/code&gt;&lt;br&gt;"; helpString += "-r (number) or -R (number): Roll (number) of Ring Dice&lt;br&gt;"; helpString += "-s (number) or -S (number): Roll (number) of Skill Dice&lt;br&gt;"; helpString += "-o (number) or -O (number): Reroll (number) of dice (Letter o/O, NOT 0)&lt;br&gt;"; helpString += "-l (s|a|b|x) or -L (s|a|b|x): Flags for reroll logic, reroll on (a)dvantage, (b)lanks, (s)uccess and/or (x) strife.&lt;br&gt;"; //Not yet implemented //helpString += "-a ((number)a | (number)s | (number)x) or -A ((number)a | (number)s | (number)x): Additions to final result (a)dvantage, (s)uccess, and (x) strife.&lt;br&gt;"; //helpString += "EXAMPLE: To add 1 advantage and 1 success, this would be done as -a 1a 1s not -a 1a -a 1s&lt;br&gt;"; helpString += "-g (on|off) or -G (on|off): Enable or disable graphic images (defaults to on)&lt;br&gt;" helpString += "-i (s|m|l|x) or -I (s|m|l|x): Flags for setting the size of images (defaults to s)&lt;br&gt;"; return helpString; } If there's any improvements I can make, please let me know, and thank you for your time. EDIT: Forgot to add caller to pass who's making the Roll.&nbsp; Added that in.
Updated to v0.4: -Added rerolls for when you have a success (for Adversities) -Fixed a bug with the Graphics option not working
This is a fantastic tool! A great start for getting these particular funky dice working in roll20.
Thanks, grinnock!&nbsp; Glad to hear people are using it while the beta is going on.&nbsp; I'm going to try to get some more work on it this weekend, depending on other circumstances.&nbsp; I'll try to hit the graphic resizing options and the add successes/advantage/strife results this weekend, but if anyone sees anything that stands out as a missing feature, let me know!
One question, I'm implementing your script in a macro for the players. Is it possible to send the ring and skill values as variables when called from the chat? My codebrain is rusty, and I'm not finding good info for doing this when I google it.
1508446719
The Aaron
Pro
API Scripter
If you are adding this as an ability on a character, you can use @{ring} and @{skill} (assuming those are the attribute names on the character). If you're adding it as an actual macro on the collections tab, you need to use @{selected|ring} and @{selected|skill} or @{target|ring} and @{target|skill}.
Good news!&nbsp; Just as promised, I've updated the dice roller to include re-sizable Dice images (with 30x30, 40x40, 50x50, and 62x62 sizes) and added the add results component, with additional images for that.
1508772577

Edited 1508772605
The Aaron
Pro
API Scripter
I bet you would get a bunch of mileage out of underscore.js (underscorejs.org), which is conveniently accessible via the _ global variable in the API...
No updates this week on the list, but I am working on the keep dice logic: it'll display three results: Maximum number of successes on the roll, Maximum number of advantage with a provided Target Number (defaults to TN 1, but I'd think 2 might be more reasonable, let me know what you think!&nbsp; Also, need to consider how to handle Hidden TNs that GMs can do to provide Void Points to players) as well as Minimum Strife.&nbsp; I've also adding a way to keep track of the 'Compromised' state that was added to the Beta to this keep roll, which will change the output. Also, The Aaron, you're right, I would!&nbsp; Might be worth rewriting to take advantage of that to clean up the code!
So, after a bit longer than I thought it'd take (mostly real life crap getting in the way), I present Version 0.6, which partially implements the keep logic (just for Maximum Successes).&nbsp; There's a few other things I'd have to add in, and development's slowing down a bit, but once I get the logic implemented and with options to disable the keep logic, I'll remove the older 0.5 version (leaving that version available for those who don't want the keep dice logic).