Here is a fully annotated version: // This registers a function to run when the 'ready' event occurs
// 'ready' happens when the API Sandbox has fully loaded all the elements of the current game
// [ <a href="https://wiki.roll20.net/API:Events#Campaign_Events" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Events</a> ]
on('ready',function(){
// This tells Javascript to enforce certain strict rules when evaluating the code (generally things that avoid errors).
"use strict";
// Setting up variables. These exist in the current function and for all functions defined within it.
// These are in a Javascript Closure, something you may want to read up on.
// [ <a href="http://javascriptissexy.com/understand-javascript-closures-with-ease/" rel="nofollow">http://javascriptissexy.com/understand-javascript-closures-with-ease/</a> ]
var affectedTokens={}, //< an object to store tokens that are in the rotation to receive random animations
minSeconds = 5, //< the minimum time to wait before applying another effect
maxSeconds = 15, //< the maximum time to wait before applying another effect
// Arrays of choices for building a random effect
eColor = [ 'acid', 'blood', 'death', 'fire', 'frost', 'holy', 'slime' ],
eType = [ 'bubblingcauldron','burn','burst','explosion','healinglight','holyfire','nova','smokebomb','splatter' ],
// a function that assembles a random effect name
randomEffect=function(){
// _.sample() returns a random subset from a collection. By default, it returns a single value.
// [ <a href="http://underscorejs.org/#sample" rel="nofollow">http://underscorejs.org/#sample</a> ]
//
// This will return a string such as 'burn-acid' or 'nova-frost'
return _.sample(eType)+'-'+_.sample(eColor);
},
// a function that creates a random effect at a particular location (x,y) on a particular page (p)
applyEffect=function(x,y,p){
// spawnFx() creates an effect at a point on a page.
// [ <a href="https://wiki.roll20.net/API:Utility_Functions#Special_Effects_.28FX.29" rel="nofollow">https://wiki.roll20.net/API:Utility_Functions#Special_Effects_.28FX.29</a> ]
spawnFx(x,y,randomEffect(),p);
};
// This registers a function to run when the 'chat:message' event occurs
// 'chat:message' happens whenever anything is put into the chat box by anyone connected to the game
// [ <a href="https://wiki.roll20.net/API:Chat#Chat_Events" rel="nofollow">https://wiki.roll20.net/API:Chat#Chat_Events</a> ]
on('chat:message',function(msg){
// do some checks to make sure we want to run for this message
// 'api' message type is for messages that begin with '!'
// playerIsGM() will insure that only a gm can run this command
// [ <a href="https://wiki.roll20.net/API:Utility_Functions#Player_Is_GM" rel="nofollow">https://wiki.roll20.net/API:Utility_Functions#Player_Is_GM</a> ]
//
// .match(/^!randomfx/) checks to make sure the command is the right one.
if('api' === msg.type && playerIsGM(msg.playerid) && msg.content.match(/^!randomfx/) ){
// _.chain() is a powerful functional programming method in underscore.js
// The basic idea is you take a collection as the start of a chain and
// perform various operations on it. The input to an operation is the
// output from the previous and so on.
// [ <a href="http://underscorejs.org/#chaining" rel="nofollow">http://underscorejs.org/#chaining</a> ]
//
// msg.selected is an array of { _type: <typename> ,_id: <id> } objects which
// describe the currently selected objects.
_.chain(msg.selected)
// .map() takes a collection and call a function on each element, then returns
// an array containing the result of each call. Using .chain(), the first argument
// behind the scenes is the collection in the chain.
// [ <a href="http://underscorejs.org/#map" rel="nofollow">http://underscorejs.org/#map</a> ]
//
// msg.selected -> .map() -> all the Roll20 graphics that were selected
.map(function(o){
// This will get the Roll20 graphic associated with the selected id.
// If what was selected wasn't a graphic (a drawing, some text, etc)
// it will return the javascript identifier 'undefined'.
// [ <a href="https://wiki.roll20.net/API:Objects#getObj.28type.2C_id.29" rel="nofollow">https://wiki.roll20.net/API:Objects#getObj.28type.2C_id.29</a> ]
return getObj('graphic',o._id);
})
// .compact() returns the collection it was given without any of the 'falsy'
// entries, things like 'undefined', 'false', 'null', etc. Using .chain(), the argument
// behind the scenes is the collection in the chain.
// [ <a href="http://underscorejs.org/#compact" rel="nofollow">http://underscorejs.org/#compact</a> ]
//
// selected graphics and undefined -> .compact() -> selected graphics
.compact()
// .each() is just like .map(), except what it returns is what it was given.
// Effectively, it gives you the opportunity to do something with each element
// without changing the list. Using .chain(), the first argument
// behind the scenes is the collection in the chain.
// [ <a href="http://underscorejs.org/#each" rel="nofollow">http://underscorejs.org/#each</a> ]
//
// selected graphics -> .each() -> selected graphics
.each(function(t){
// Now that we've filtered down the results to just selected graphics, the real work
// starts.
// _.has() is an alias for <object>.hasOwnProperty() which simply takes a given object
// and returns 'true' if it contains the specified property. In this case, I'm using
// the id of the graphic as the property name, effectively treating the affectedTokens
// object as a hash.
// [ <a href="http://underscorejs.org/#has" rel="nofollow">http://underscorejs.org/#has</a> ]
if(_.has(affectedTokens,t.id)){
// If affectedTokens has the id in it, it means effects are already happening for
// that token, so I want to remove it from the rotation of things having effects.
// delete <object>[<property name>] removes that property from the object, effectively
// removing this token from the hash.
delete affectedTokens[t.id];
} else {
// If affectedTokens didn't have the id, it means I'm adding it into rotation.
// I set the property for the graphic's id to be true. I considered
// storing an object with some timing information instead of true, which
// is why I made affectedTokens an object instead of a simple array, but
// the implementation went another way.
affectedTokens[t.id]=true;
// This creates another Javascript Closure to capture the id and page number and
// save them for executions of the function I'll be defining within it. Technically,
// the function on each is already a Closure for this function so I probably didn't
// need another one here. I have a tendency to over Closure, but it's not generally a
// problem to do so and likely the encapsulation makes it somewhat clearer... maybe. =D
//
// 'id' and 'p' here are what is passed further down, t.id and t.get('pageid').
// This is an Immediately Invoking Function Expression (IIFE or "iffy")
// [ <a href="https://en.wikipedia.org/wiki/Immediately-invoked_function_expression" rel="nofollow">https://en.wikipedia.org/wiki/Immediately-invoked_function_expression</a> ]
//
// Basically, this function gets created and executed all in one, making a Closure to
// save the parameters for use internally.
(function (id,p){
// Here I'm creating a function that will apply effects to the current graphic and also deal
// with kicking off the next effect on the current graphic. A different copy of this function
// will get created for each graphic, safely in it's own Closure.
var effectRunner=function(){
// First, check if the graphic is still in rotation. If a later execution of
// !randomfx removed the id from affectedTokens, that means I want to stop
// running the effects. Doing nothing (not going into this if block) means
// no further timers are set and the function stops getting executed.
//
// I could have used _.has() here, or used this style above, they are almost equivalent
// and are interchangeable in almost every circumstance.
if(affectedTokens[id]){
// Grab the graphic for this id
var t = getObj('graphic',id);
// If we found it, we'll do some things (otherwise, it was deleted and we'll stop)
if(t){
// With the graphic in hand, we can now apply an effect to it's current position.
applyEffect(t.get('left'),t.get('top'),p);
// Next we set a timer to run this function again. setTimeout() is asynchronous, so
// we don't need to worry about stack depth for recursion.
//
// randomInteger() returns a number between 1 and the supplied argument, like rolling
// a die. Technically, this means the minimum time between effects is
// (minSeconds seconds + 1 millisecond), but hopefully you'll forgive that oversight!
// [ <a href="https://wiki.roll20.net/API:Utility_Functions#Random_Numbers" rel="nofollow">https://wiki.roll20.net/API:Utility_Functions#Random_Numbers</a> ]
setTimeout(
effectRunner,
randomInteger((maxSeconds-minSeconds)*1000)+(minSeconds*1000)
);
} else {
// In the case where we didn't find the graphic, it means someone deleted it. For that
// case, we want to stop the rotation on the graphic be removing it from the
// collection of affectedTokens.
delete affectedTokens[id];
}
}
};
// Start the rotation for this graphic by running the created function. This will trigger the
// first effect and set a random timer for the next effect.
effectRunner();
}(t.id,t.get('pageid'))); //< invoking the IIFE to set 'id' and 'p'
}
});
// End of the chain. If we wanted the results, we would call
// .value() to cause _.chain() above to return them. In this
// case we don't need the results.
// [ <a href="http://underscorejs.org/#value" rel="nofollow">http://underscorejs.org/#value</a> ]
} // end of the check for !randomfx
}); // end of the on('chat:message')
}); // end of the on('ready')
I also made some minor adjustments to the one above. Let me know if you have any questions!