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. 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. 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: /**
* Legend of the Five Rings 5e Dice Roller
* @author DTemplar5
* @version 0.6
* 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
* Also thanks to Mike W. Leavitt for his Exalted dice roller was a place I could start from.
* And thanks to you for being curious and looking at this! I've hid some easter eggs for you, care to find them?
* New in Version 0.6:
* -Added Kept dice command based on Max Success
* -Added Compromised Flag to avoid keeping Strife-laden results
* -Started Scorpion Clan Coup
**/
/**
* ToDo:
* Continue working on expanding keep logic to include Max Advantage (with getting up to TN successes first), and Min Strife
* -Add TN flag (-T or -t) to set the TN of the task; this also lets you know if you failed or succeeded
* -Doesn't work on a secret TN setup though.
* Continue Improving Results Display
* -Formatting to look better and more inline with the dice
* -Better dice images, just pulled from the beta PDF
* -Pretty up the output
* Scorpion Clan Coup
**/
log('Loading L5R Dice Roller');
sendChat('L5R API', 'The Secrets of the Dice Roller can be found by typing <code>!l5r -help</code>');
//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
]
};
/**
* Main Body Loop for catching messages
**/
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) {
if(rawCmd.indexOf("on") != -1) {
graphics = true;
sendChat("L5RAPI", "The Graphics has been turned on");
} else {
graphics = false;
sendChat("L5RAPI", "The Graphics has been turned off");
}
} 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) {
var outHTML = buildHelp();
sendChat('EX3Dice API', '/w ' + msg.who + ' ' + outHTML);
} else {
printError(msg, msg.who);
}
}
});
/**
* splitCommands, parsing out each segment of the commands present and parsing them individually, rolling each dice segment and rerolling as needed
* Not very clean, a bit bodgey, but uses RegEx to catch the first instance of each entry,
* so doing -r 4 -r 5 will result in -r 4 getting rolled, and ignoring -r 5 completely.
**/
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 < 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);
}
/**
* rollDice, the actual dice rolling with the commands parsed
**/
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 < 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 < rerollDice && result[0] == "B") ||
(logicAdvantage && rerollArray.length < rerollDice && result[0].indexOf("A") == 0) ||
(logicStrife && rerollArray.length < rerollDice && result[0].indexOf("X") != -1) ||
(logicSuccess && rerollArray.length < 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 < 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 < 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 < 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! 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 < rerollDice && result[0] == "B") ||
(logicAdvantage && rerollArray.length < rerollDice && result[0].indexOf("A") == 0) ||
(logicStrife && rerollArray.length < rerollDice && result[0].indexOf("X") != -1) ||
(logicSuccess && rerollArray.length < 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 < 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 < 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);
}
/**
* printError, a simplified form of sending error messages
**/
function printError(result, sender) {
log('Error!');
if (result.type == 'error' ) {
sendChat('L5R API', '/w ' + sender + ' The peasents in charge of Roll20 could not obey. They said: ' + result.content);
} else {
sendChat('L5R API', '/w ' + sender + ' The eta did not understand your command. Please try again.');
}
}
/**
* getDiceString, turn the array values into readable text
**/
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 < diceArray.length; ++i) { //Yes, inefficent, would be best to pass this from before
diceCount += diceArray[i].length;
}*/ //Deprecated! Woot!
var returnString = "/direct <table border=\"1\"><tr><th colspan=\"" + diceCount + "\">Results</th></tr><td align=\"center\" colspan=\"" + diceCount + "\">" + caller + "</td></tr><tr>";
for(var i = 0; i < diceArray.length; ++i) {
var diceResult = "<td>"; //TODO: Put wrapper if length > 1
for(var j = 0; j < diceArray[i].length; ++j) {
diceResult += diceArray[i][j];
}
returnString += diceResult + "</td>"; //TODO: Put closer if length > 1
diceResult = "";
}
returnString += "</tr>";
if(rerollArray) { //Check if it exists first
returnString += "<tr><td colspan=\"" + diceCount + "\">";
if(rerollArray.length > 0) {
returnString += "Rerolled Results: ";
for(var i = 0; i < rerollArray.length; ++i) {
returnString += rerollArray[i] + " ";
}
}
returnString += "</td></tr>";
}
if(extraSuccess > 0 || extraAdvantage > 0 || extraStrife > 0) { //Add row for extra values added to the result
returnString += "<tr><td colspan=\"" + diceCount + "\">Added Results: ";
log(returnString.length);
var j = 0; //Iterator
if(graphics) {
while(j < extraSuccess) { //Skip if over 0
returnString += imageString(display.graphics.result.suc);
++j;
}
j = 0;
while(j < extraAdvantage) {
returnString += imageString(display.graphics.result.adv);
++j;
}
j = 0;
while(j < extraStrife) {
returnString += imageString(display.graphics.result.strife);
++j;
}
} else {
if(extraSuccess > 0) {
returnString += extraSuccess + "S ";
}
if(extraAdvantage > 0) {
returnString += extraAdvantage + "A ";
}
if(extraStrife > 0) {
returnString += extraStrife + "X";
}
}
returnString += "</td></tr>";
}
//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 += "</table>";
return returnString;
}
/**
* imageString, helper function to build the HTML to display the dice result
**/
function imageString(imgStr) {
return "<img src=\"" + imgStr + "\" height=\"" + display.size.C + "\" width=\"" + display.size.C + "\" />";
}
/**
* convertImage, helper function to convert a text result to the image file
**/
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";
}
/**
*buildHelp, the function to return Help formatted in an easy to understand fashion
*ToDo: Revise help to be pretty looking
**/
function buildHelp() {
var helpString = "";
helpString = "<code>!l5r [cmd] [cmdoption] [cmd] [cmdoption]...</code><br>";
helpString += "-r (number) or -R (number): Roll (number) of Ring Dice<br>";
helpString += "-s (number) or -S (number): Roll (number) of Skill Dice<br>";
helpString += "-o (number) or -O (number): Reroll (number) of dice (Letter o/O, NOT 0)<br>";
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.<br>";
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.<br>";
helpString += "EXAMPLE: To add 1 advantage and 1 success, this would be done as -a 1a 1s not -a 1a -a 1s<br>";
helpString += "-g (on|off) or -G (on|off): Enable or disable graphic images (defaults to on)<br>"
helpString += "-i (s|m|l|x) or -I (s|m|l|x): Flags for setting the size of images (defaults to s)<br>";
helpString += "-c: Compromised status flag (Prevent keep logic from keeping strife results)<br>";
return helpString;
}
/**
*keptDice, the function to generate logic based on which dice are kept or not
*Returns a string to add to the results
*/
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) {
//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) {
for(var i = 0; i < die.length; ++i) {
switch(die[i][0]) {
case 6:
case 7:
case 9: die.splice(i, 1); break;
default: break; //Do nothing
}
}
});
if(successArray.length > 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 = "<tr><td align=\"center\" colspan=\"" + diceArray.length + "\">Max Success Results</td></tr><tr>";
_.each(successArray, function(die) {
maxSuccess += "<td>";
for(var x = 0; x < die.length; ++x) {
maxSuccess += convertImage(die[x][0], die[x][1]);
}
maxSuccess += "</td>";
});
for(var x = successArray.length; x < diceArray.length; ++x) {
maxSuccess += "<td> </td>"; //Padding
}
maxSuccess += "</tr>";
return maxSuccess;
}
Old Version (keeping for now): /**
* Legend of the Five Rings 5e Dice Roller
* @author DTemplar5
* @version 0.5
* 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
* Also thanks to Mike W. Leavitt for his Exalted dice roller was a place I could start from.
* And thanks to you for being curious and looking at this! I've hid some easter eggs for you, care to find them?
**/
/**
* ToDo:
* Improving Results Display
* -Formatting to look better and more inline with the dice
* -Showing different Kept Results (Max Success, Max Advantage, Min Strife)
* -Better dice images, just pulled from the beta PDF
* Scorpion Clan Coup
**/
log('Loading L5R Dice Roller');
sendChat('L5R API', 'The Secrets of the Dice Roller can be found by typing <code>!l5r -help</code>');
//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
]
};
/**
* Main Body Loop for catching messages
**/
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) {
if(rawCmd.indexOf("on") != -1) {
graphics = true;
sendChat("L5RAPI", "The Graphics has been turned on");
} else {
graphics = false;
sendChat("L5RAPI", "The Graphics has been turned off");
}
} 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) {
var outHTML = buildHelp();
sendChat('EX3Dice API', '/w ' + msg.who + ' ' + outHTML);
} else {
printError(msg, msg.who);
}
}
});
/**
* splitCommands, parsing out each segment of the commands present and parsing them individually, rolling each dice segment and rerolling as needed
* Not very clean, a bit bodgey, but uses RegEx to catch the first instance of each entry,
* so doing -r 4 -r 5 will result in -r 4 getting rolled, and ignoring -r 5 completely.
**/
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 < 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);
}
/**
* rollDice, the actual dice rolling with the commands parsed
**/
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 < 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 < rerollDice && result[0] == "B") ||
(logicAdvantage && rerollArray.length < rerollDice && result[0].indexOf("A") == 0) ||
(logicStrife && rerollArray.length < rerollDice && result[0].indexOf("X") != -1) ||
(logicSuccess && rerollArray.length < 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 < 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 < 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! 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 < rerollDice && result[0] == "B") ||
(logicAdvantage && rerollArray.length < rerollDice && result[0].indexOf("A") == 0) ||
(logicStrife && rerollArray.length < rerollDice && result[0].indexOf("X") != -1) ||
(logicSuccess && rerollArray.length < 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 < 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);
}
/**
* printError, a simplified form of sending error messages
**/
function printError(result, sender) {
log('Error!');
if (result.type == 'error' ) {
sendChat('L5R API', '/w ' + sender + ' The peasents in charge of Roll20 could not obey. They said: ' + result.content);
} else {
sendChat('L5R API', '/w ' + sender + ' The eta did not understand your command. Please try again.');
}
}
/**
* getDiceString, turn the array values into readable text
**/
function getDiceString(diceArray, rerollArray, extraSuccess, extraAdvantage, extraStrife, caller) {
//Get Count of dice
var diceCount = 0;
for(var i = 0; i < diceArray.length; ++i) { //Yes, inefficent, would be best to pass this from before
diceCount += diceArray[i].length;
}
var returnString = "/direct <table border=\"1\"><tr><th colspan=\"" + diceCount + "\">Results</th></tr><td colspan=\"" + diceCount + "\">" + caller + "</td></tr><tr>";
for(var i = 0; i < diceArray.length; ++i) {
var diceResult = "<td>"; //TODO: Put wrapper if length > 1
for(var j = 0; j < diceArray[i].length; ++j) {
diceResult += diceArray[i][j];
}
returnString += diceResult + "</td>"; //TODO: Put closer if length > 1
diceResult = "";
}
returnString += "</tr>";
if(rerollArray) { //Check if it exists first
returnString += "<tr><td colspan=\"" + diceCount + "\">";
if(rerollArray.length > 0) {
returnString += "Rerolled Results: ";
for(var i = 0; i < rerollArray.length; ++i) {
returnString += rerollArray[i] + " ";
}
}
returnString += "</td></tr>";
}
if(extraSuccess > 0 || extraAdvantage > 0 || extraStrife > 0) { //Add row for extra values added to the result
returnString += "<tr><td colspan=\"" + diceCount + "\">Added Results: ";
log(returnString.length);
var j = 0; //Iterator
if(graphics) {
while(j < extraSuccess) { //Skip if over 0
returnString += imageString(display.graphics.result.suc);
++j;
}
j = 0;
while(j < extraAdvantage) {
returnString += imageString(display.graphics.result.adv);
++j;
}
j = 0;
while(j < extraStrife) {
returnString += imageString(display.graphics.result.strife);
++j;
}
} else {
if(extraSuccess > 0) {
returnString += extraSuccess + "S ";
}
if(extraAdvantage > 0) {
returnString += extraAdvantage + "A ";
}
if(extraStrife > 0) {
returnString += extraStrife + "X";
}
}
returnString += "</td></tr>";
}
returnString += "</table>";
return returnString;
}
/**
* imageString, helper function to build the HTML to display the dice result
**/
function imageString(imgStr) {
return "<img src=\"" + imgStr + "\" height=\"" + display.size.C + "\" width=\"" + display.size.C + "\" />";
}
/**
* convertImage, helper function to convert a text result to the image file
**/
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";
}
/**
* buildHelp, the function to return Help formatted in an easy to understand fashion
* ToDo: Revise help to be pretty looking
**/
function buildHelp() {
var helpString = "";
helpString = "<code>!l5r [cmd] [cmdoption] [cmd] [cmdoption]...</code><br>";
helpString += "-r (number) or -R (number): Roll (number) of Ring Dice<br>";
helpString += "-s (number) or -S (number): Roll (number) of Skill Dice<br>";
helpString += "-o (number) or -O (number): Reroll (number) of dice (Letter o/O, NOT 0)<br>";
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.<br>";
//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.<br>";
//helpString += "EXAMPLE: To add 1 advantage and 1 success, this would be done as -a 1a 1s not -a 1a -a 1s<br>";
helpString += "-g (on|off) or -G (on|off): Enable or disable graphic images (defaults to on)<br>"
helpString += "-i (s|m|l|x) or -I (s|m|l|x): Flags for setting the size of images (defaults to s)<br>";
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. Added that in.