diff --git a/HiddenRolls/Help.txt b/HiddenRolls/Help.txt new file mode 100644 index 0000000000..26fe6497bf --- /dev/null +++ b/HiddenRolls/Help.txt @@ -0,0 +1,95 @@ +HiddenRolls + +HiddenRolls is a package of commands for hiding roll information in various +ways. Inline rolls can be modified to display only the total (hiding the +formula), only the roll values (hiding the formula and totals), or only the +formula (whispering the totals to the GM). + +It is recommended that this script be used in conjunction with the CommandShell +module, which will improve output formatting and command discovery. + + +Commands: + + !hideroll [options] command + !hiderolls [options] command + Replaces all inline rolls in command with their respective totals. + + !hidetotal [options] command + !hidetotals [options] command + Replaces all inline rolls in command with new expressions containing + only the dice from the original expressions (i.e. no bonuses or + annotations). The full expression (with totals) is whispered to the GM. + + !hideall [options] command + Forwards command to the GM. Whispers command to the sender, with all + inline rolls removed (and replaced with their respective formulas). + +The above commands accept the following options: + + -h, --help Displays a help message and exits. + + -v, --verbose Generates additional output, depending on the command: + !hideroll(s): Whispers full results (including + rolls and formulas) to the GM. + !hidetotal(s): Whispers full results to the + sender in addition to the GM. + !hideall: Sends the stripped command to + global chat instead of + whispering it to the sender. + + +Notes: + +In order to guarantee the integrity of roll results, the Roll20 API does not +provide a way to insert roll results into an inline roll. As a result, +HiddenRolls displays dice results as "{1d0+V1+V2+...}[XdY] ", where V1, V2, etc. +are roll values and XdY is the dice expression. For example, "[[1d20+7]]" would +be displayed as "[[{1d0+12}[1d20] +7]]". The "1d0" allows the expression to +function like a dice expression (otherwise the label would generate errors in +certain conditions), and the label allows the GM to verify the roll formula. + +Roll20's chat system also automatically consumes template expressions before +forwarding messages to scripts. As a result, expressions like +"&{template:default}" will never be seen by scripts. To work around this, +HiddenRolls replaces all instances of "&" with "&". To use a roll template, +simply specify it as "&{template:...}". + + +Examples: + +Below are some example commands, along with the chat commands they will +generate. Note that the whispers below are sent by the API, so they will not +appear in the sender's chat log unless they are the target of the whisper. + +!hideroll The ogre swings its club ([[1d20+7]] to hit), doing [[2d6+5]] damage + The ogre swings its club ([[19]] to hit), doing [[13]] damage + +!hiderolls -v The fireball explodes for [[5d6]] damage + The fireball explodes for [[17]] damage + /w gm The fireball explodes for [[{1d0+1+3+6+5+2}[5d6] ]] damage + +!hidetotal The troll makes a toughness save: [[1d20+8]] + The troll makes a toughness save: [[{1d0+15}[1d20] +0[hidden] ]] + /w gm The troll makes a toughness save: [[{1d0+15}[1d20] +8]] + +!hidetotals -v The enchanter's spell does [[2d8+2]] damage + The enchanter's spell does [[{1d0+7+3}[2d8] +0[hidden] ]] damage + /w gm The enchanter's spell does [[{1d0+7+3}[2d8] +2]] damage + /w Tim The enchanter's spell does [[{1d0+7+3}[2d8] +2]] damage + +!hideall Slipping stealthily into a sneaky stance: [[1d20+12]] + /w gm Slipping stealthily into a sneaky stance: [[1d20+12]] + /w Snake Slipping stealthily into a sneaky stance: [1d20+12] + Note the single brackets: the expression is not an inline roll, and is + present only to display the formula being rolled. + +!hideall -v Is it live or is it Magicex? [[1d20+8]] will save. + /w gm Is it live or is it Magicex? [[1d20+8]] will save. + Is it live or is it Magicex? [1d20+8] will save. + Note the single brackets: the expression is not an inline roll, and is + present only to display the formula being rolled. + +!hideall &{template:default} {{name=Skill Check}} {{Hide=[[1d20+12]]}} + /w gm &{template:default} {{name=Skill Check}} {{Hide=[[1d20+12]]}} + /w Snake {template:default} {{name=Skill Check}} {{Hide=[1d20+12]}} diff --git a/HiddenRolls/hidden.js b/HiddenRolls/hidden.js new file mode 100644 index 0000000000..dd3bddb797 --- /dev/null +++ b/HiddenRolls/hidden.js @@ -0,0 +1,216 @@ +var HiddenRolls = HiddenRolls || { + COMMANDS: ["!hideroll", "!hiderolls", "!hidetotal", "!hidetotals", "!hideall", "!help"], + + write: function(s, who, style, from){ + if (who){ + who = "/w " + who.split(" ", 1)[0] + " "; + } + sendChat(from, who + s.replace(//g, ">").replace(/\n/g, "
")); + }, + + showHelp: function(who){ + var helpMsg = ""; + helpMsg += "Usage: !hideroll(s) [options] command\n"; + helpMsg += " or: !hidetotal(s) [options] command\n"; + helpMsg += " or: !hideall [options] command\n"; + helpMsg += "!hideroll and !hiderolls hide roll results, only displaying the total.\n"; + helpMsg += "!hidetotal and !hidetotals hide result totals, only displaying the roll values.\n"; + helpMsg += "!hideall hides rolls and totals, only displaying the roll formula.\n"; + HiddenRolls.write(helpMsg, who, "", "HR"); + helpMsg = "Options:\n"; + helpMsg += " -h, --help: display this help message\n"; + helpMsg += " -v, --verbose: generate additional output as follows:\n"; + helpMsg += " !hideroll(s): whisper full results to GM\n"; + helpMsg += " !hidetotal(s): whisper full results to sender\n"; + helpMsg += " !hideall: send expression to global chat\n"; + HiddenRolls.write(helpMsg, who, "font-size: small; font-family: monospace", "HR"); + }, + + generateExpression: function(who, rolls, diceOnly){ + if (!rolls){ return null; } + + var retval = ""; + for (var i = 0; i < rolls.length; i++){ + var r = rolls[i]; +///// +// + //handle r.mods; for now, just acknowledge that we can't + if (r.mods){ return null; } +// +///// + switch(r.type){ + case "C": + // comment; just append it onto the expression + retval += r.text; + break; + case "M": + // math; just append it onto the expression + if ((diceOnly) && (r.expr.length > 1)){ + retval += "+0[hidden] "; + } + else{ + retval += r.expr; + } + break; + case "L": + // label; add it to the expression as a label + if (diceOnly){ break; } + retval += "[" + r.text + "] "; + break; + case "R": + // roll; add result of each die to 1d0 so we can label the results like dice + retval += "{1d0"; + for (var j = 0; j < r.results.length; j++){ + retval += "+" + r.results[j].v; + if (r.results[j].d){ + // dropped die; display as "Vd0", where V was the value + retval += "d0"; + } + } + retval += "}"; + retval += "[" + r.dice + "d" + r.sides + "] "; + break; + case "G": + var subExp = HiddenRolls.generateExpression(who, r.rolls[0], diceOnly); + if (!subExp){ return null; } + retval += "{" + subExp; + for (var j = 1; j < r.rolls.length; j++){ + subExp = HiddenRolls.generateExpression(who, r.rolls[j], diceOnly); + if (!subExp){ return null; } + retval += "," + subExp; + } + retval += "}"; + break; + default: + // unrecognized type; just return the expression unmodified + HiddenRolls.write("Unrecognized roll result type: " + r.type, who, "", "HiddenRolls"); + return null; + } + } + return retval; + }, + + handleHideMessage: function(tokens, msg){ + if (tokens.length < 2){ + return HiddenRolls.showHelp(msg.who); + } + + var verbose = false; + var cmd = msg.content.replace(/^\S+\s+/, ""); + var inlineRolls = msg.inlinerolls || []; + + if ((tokens[1] == "-h") || (tokens[1] == "--help")){ + return HiddenRolls.showHelp(msg.who); + } + + if ((tokens[1] == "-v") || (tokens[1] == "--verbose")){ + verbose = true; + cmd = cmd.replace(/^\S+\s+/, ""); + } + + cmd = cmd.replace(/&/g, "&"); + + switch(tokens[0]){ + case "!hideroll": + case "!hiderolls": + // hide rolls; display only totals (e.g. "ogre attacks: [[1d20+7]] to hit" => "ogre attacks: [[18]] to hit") + var replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].results)){ return s; } + return "[[" + inlineRolls[i].results.total + "]]"; + }; + sendChat(msg.who, cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines)); + if (verbose){ + // verbose mode: whisper original formula to gm with roll results substituted in + replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].results)){ return s; } + var expr = HiddenRolls.generateExpression(msg.who, inlineRolls[i].results.rolls); + return (expr ? "[[" + expr + "]]" : inlineRolls[i].expression); + }; + sendChat(msg.who, "/w gm " + cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines)); + } + break; + case "!hidetotal": + case "!hidetotals": + // hide totals; display only dice rolls + var replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].results)){ return s; } + var expr = HiddenRolls.generateExpression(msg.who, inlineRolls[i].results.rolls, true); + return (expr ? "[[" + expr + "]]" : inlineRolls[i].expression); + }; + sendChat(msg.who, cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines)); + // whisper totals to gm + replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].results)){ return s; } + var expr = HiddenRolls.generateExpression(msg.who, inlineRolls[i].results.rolls); + return (expr ? "[[" + expr + "]]" : inlineRolls[i].expression); + }; + var hiddenCmd = cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines); + sendChat(msg.who, "/w gm " + hiddenCmd); + if ((verbose) && (!playerIsGM(msg.playerid))){ + // verbose mode: whisper totals to original sender too + sendChat(msg.who, "/w " + msg.who.split(" ", 1)[0] + " " + hiddenCmd); + } + break; + case "!hideall": + // whisper results to gm + var replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].expression)){ return s; } + return "[[" + inlineRolls[i].expression + "]]"; + }; + sendChat(msg.who, "/w gm " + cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines)); + replaceInlines = function(s){ + if (!inlineRolls){ return s; } + var i = parseInt(s.substring(3, s.length - 2)); + if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i].expression)){ return s; } + return "[" + inlineRolls[i].expression + "]"; + }; + var cmdExpr = cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines); + if (verbose){ + // verbose mode: send expression to global chat + sendChat(msg.who, cmdExpr); + } + else if (!playerIsGM(msg.playerid)){ + // normal mode: whisper expression to sender + sendChat(msg.who, "/w " + msg.who.split(" ", 1)[0] + " " + cmdExpr); + } + break; + default: + return HiddenRolls.showHelp(msg.who); + } + }, + + handleChatMessage: function(msg){ + if ((msg.type != "api") || (HiddenRolls.COMMANDS.indexOf(msg.content.split(" ", 1)[0]) < 0)){ return; } + + return HiddenRolls.handleHideMessage(msg.content.split(" "), msg); + }, + + registerHiddenRolls: function(){ + if ((typeof(Shell) != "undefined") && (Shell) && (Shell.registerCommand)){ + Shell.registerCommand("!hideroll", "!hideroll [--verbose] command", "Display only roll totals in command", HiddenRolls.handleHideMessage); + Shell.registerCommand("!hiderolls", "!hiderolls [--verbose] command", "Display only roll totals in command", HiddenRolls.handleHideMessage); + Shell.registerCommand("!hidetotal", "!hidetotal [--verbose] command", "Display only raw rolls in command", HiddenRolls.handleHideMessage); + Shell.registerCommand("!hidetotals", "!hidetotals [--verbose] command", "Display only raw rolls in command", HiddenRolls.handleHideMessage); + Shell.registerCommand("!hideall", "!hideall [--verbose] command", "Only show results of command to GM", HiddenRolls.handleHideMessage); + Shell.permissionCommand(["!shell-permission", "add", "!hideall"], {'who': "HR"}); + if (Shell.write){ + HiddenRolls.write = Shell.write; + } + } + else{ + on("chat:message", HiddenRolls.handleChatMessage); + } + } +}; + +on("ready", function(){ HiddenRolls.registerHiddenRolls(); }); diff --git a/HiddenRolls/package.json b/HiddenRolls/package.json new file mode 100644 index 0000000000..db039fd2c3 --- /dev/null +++ b/HiddenRolls/package.json @@ -0,0 +1,10 @@ +{ + "name": "HiddenRolls", + "version": "0.2", + "description": "Various types of roll hiding (unmodified roll only, final result only, only show GM)", + "authors": "manveti", + "roll20userid": "503018", + "dependencies": {}, + "modifies": {}, + "conflicts": [] +} diff --git a/cron/Help.txt b/cron/Help.txt index b2bc7a5246..767b189db9 100644 --- a/cron/Help.txt +++ b/cron/Help.txt @@ -100,3 +100,7 @@ Examples: This will remove job 42 after 10 rounds. Note that the quotes are required around commands which contain tokens which are valid !cron arguments (e.g. "-R"). Without the quotes, this will immediately remove job 42. + +!cron -a 1 &{template:default} {{name=Test}} {{foo=bar}} + Note that the "&" must be "&", otherwise the "&{template:default}" will + be consumed by the chat system before the message is passed to the script. diff --git a/cron/cron.js b/cron/cron.js index 7c1c0fe4ac..eb2f88d0a8 100644 --- a/cron/cron.js +++ b/cron/cron.js @@ -279,7 +279,7 @@ var cron = cron || { if ((i < 0) || (i >= inlineRolls.length) || (!inlineRolls[i]) || (!inlineRolls[i]['expression'])){ return s; } return "[[" + inlineRolls[i]['expression'] + "]]"; } - return cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines); + return cmd.replace(/\$\[\[\d+\]\]/g, replaceInlines).replace(/&/g, "&"); }, handleCronMessage: function(tokens, msg){ diff --git a/cron/package.json b/cron/package.json index ff65418041..636be68f44 100644 --- a/cron/package.json +++ b/cron/package.json @@ -1,6 +1,6 @@ { "name": "cron", - "version": "0.1", + "version": "0.2", "description": "Schedule (possibly recurring) commands to run at some point in the future.", "authors": "manveti", "roll20userid": "503018",