Skip to content

Instantly share code, notes, and snippets.

@mattkauffman23
Last active February 17, 2017 06:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mattkauffman23/cf179c9fe71b6e01c040 to your computer and use it in GitHub Desktop.
Save mattkauffman23/cf179c9fe71b6e01c040 to your computer and use it in GitHub Desktop.
Roll 20 API Script for generating complex random items
(function() {
/*
!randomizer - Allows flexible generation of all sorts of randomizable things
such as NPCs, loot, weather, etc...
Requires the following other scripts to be installed:
- Shell (https://wiki.roll20.net/Script:Command_Shell)
Instructions:
Randomizer relies on special names for rollable tables. It intends to be
flexible and easy to use. Its simplest command is “!randomizer <TableName>”
which rolls on the table whose name matches <TableName> and reports back to
chat. Tables can also be grouped into sets and be rolled together. For
instance if the tables “npc-race”, “npc-class”, “npc-personality” and
“npc-appearance” exist a GM could use the command “!randomizer npc” to
generate a random npc by rolling on each of those tables and combining the
results. Further table items in tables can be used to trigger sub-rolls. A
good example of this would be loot horde table with a table item whose name
is “loot-gold-a*3,loot-items-c”. This table item name would trigger Randomizer
to roll 3 times on the table “loot-gold-a” and once on “loot-items-c”.
Randomizer supports a --b flag to broadcast results to everyone in chat. By
default results are only sent to the person who invoked the command.
Randomizer respects weighting assigned to table items.
Tables with an item named %%HIDDEN%% will not be displayed in the
results. This is useful for tables that just trigger sub-rolls, such as in
the loot horde example. This keyword is configurable below.
Randomizer is GM only by default. Command_Shell can be used to give players
access, however if any table included in a roll is not configured to
showplayers the roll will fail.
I believe you can “nest” tables as deep as you want to. For instance
“weather-july-temp” and “weather-july-percip” could be set up allowing
“!Randomizer weather july” to generate some random weather for july.
If anyone runs into issues or has any ideas to improve it please let me know!
*/
'use strict';
// === Start config ===
// The name used as the sender in chat
var from = 'randomizer';
// The character used to split table names into seperate words for querying
var delimiter = '-';
// The string (fancy word for sequence of letters and numbers) used to prevent
// a table's results from being displayed
var hideKeyWord = '%%HIDDEN%%';
// === End config ===
// Register the command
on('ready', function() {
Shell.registerCommand('!randomizer',
'!randomizer <type>...]',
'Roll on the requested tables.',
onInput);
});
var resultsTemplate = _.template(
'&{template:default} ' +
'{{name=<%= title %>}}' +
'<% _.each(results, function(result) { %>' +
'{{<%= result.name %>=<%= result.item %> (<%= result.val %>)}}' +
'<% }); %>'
);
function onInput(opts, msg) {
var receiver, query, response;
receiver = _.contains(opts, '--b') ? undefined : msg.who;
opts = _.reject(opts, function(opt) {
return opt.match(/^--/);
});
query = _.rest(opts);
if (!query.length) {
respond('!randomizer tables found: \n' + listTables().join('\n'),
receiver, from);
return;
}
response = buildResponse(query, msg.playerid);
respond(response, receiver, from);
}
function respond(msg, to, from) {
Shell.write(msg, to, undefined, from);
}
function listTables() {
return _.map(findTables(), function(table) {
return table.get('name').replace(delimiter, ' ');
});
}
function findTables(query) {
query = query || [];
return filterObjs(function(obj) {
return obj.get('_type') === 'rollabletable' &&
obj.get('name').indexOf(query.join(delimiter)) === 0;
});
}
function buildResponse(query, playerId) {
var title, tables, results;
tables = findTables(query);
if (!tables.length) {
return '!randomizer: Could not find matching rollable table.';
}
// Check permissions
if (!playerIsGM(playerId) &&
_.any(tables, function(table) { return !table.showplayers; })) {
return '!randomizer: You don\'t have permissions to make that roll.';
}
// If only one table in response omit the last argument of query to ovoid redundant display
title = tables.length === 1 ?
query.slice(0, -1).join(' ') : query.join(' ');
results = getResults(tables, query);
results = _.reject(results, function (result) {
return result.hide;
});
return resultsTemplate({ title: title, results: results });
}
function getResults(tables, query) {
var results, roll, ordinal, subTables;
// Roll on all the tables
results = _.reduce(tables, function(memo, table) {
roll = rollTable(table);
roll.name = getShortName(table, query);
memo.push(roll);
return memo;
}, []);
// Create unique names for any table which is rolled on more than once
_.each(_.groupBy(results, 'name'), function(group) {
if (group.length > 1) {
_.each(group, function(result, i) {
ordinal = i + 1;
// Good enough? Fails after 10 rolls on same table.
ordinal += ordinal === 1 ? 'st' : ordinal === 2 ? 'nd' : ordinal === 3 ? 'rd' : 'th';
result.name = ordinal + ' ' + result.name;
});
}
});
// Recursively roll any table items that have roll commands
subTables = findSubTables(results, query);
if (subTables.length) {
return results.concat(getResults(subTables, query));
} else {
return results;
}
}
function rollTable(table) {
var items, max, roll, hideFlag;
items = findObjs({ _type: 'tableitem', _rollabletableid: table.id });
// Look for specially named items to supress table results display
hideFlag = _.find(items, function(item) {
return item.get('name') === hideKeyWord;
});
items = _.without(items, hideFlag);
max = _.reduce(items, function(memo, item) {
return memo += item.get('weight');
}, 0);
roll = randomInteger(max);
for (var i = 0, weightedProgress = 0; i < items.length; i++) {
weightedProgress += parseInt(items[i].get('weight'), 10);
if (weightedProgress >= roll) {
return { val: roll, item: items[i].get('name'), hide: !!hideFlag };
}
}
}
function findSubTables(results, query) {
var subTables = [], commands, commandMatches, commandDescription,
name, times, matches;
results.forEach(function(result) {
commandDescription = '';
commands = result.item.split(',');
commandMatches = _.reduce(commands, function(memo, command) {
command = command.split('*');
name = command[0];
times = parseInt(command[1], 10) || 1;
matches = findTables(name.split(delimiter));
if (matches.length) {
_.times(times, function () { memo.push(matches[0]); });
commandDescription += times + ' X ' + getShortName(name, query) + '\n';
}
return memo;
}, []);
subTables = subTables.concat(commandMatches);
// Rewrite subtable roll syntax to be user friendly
result.item = commandDescription || result.item;
});
return subTables;
}
// Return table name parts not included in the query. If all are included
// return the last.
function getShortName(table, query) {
var path, diff, output;
table = typeof table === 'string' ? table : table.get('name');
path = table.split(delimiter);
diff = _.difference(path, query);
output = diff.length ? diff.join(' ') : _.last(path);
// Special case "name" since its used as title of default roll template.
if (output === 'name') output = '"name"';
return output;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment