I've got a few small ones. Here's a snippet that spawns a random fx using the command !randomfx : // 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_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</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="https://wiki.roll20.net/API:Events#Campaign_Event" rel="nofollow">https://wiki.roll20.net/API:Events#Campaign_Event</a>... ]
} // end of the check for !randomfx
}); // end of the on('chat:message')
}); // end of the on('ready')
Here's one that moves a token half their move in a random direction (if they have the overdrive swirly status marker). I call it DurnkMove: // run this function when the api is fully loaded. light going when the light turns green, instead of one of the yellow ones.
on('ready',function(){
// tells the javascript engine to make certain sloppy coding practices into errors
"use strict";
// starts defining things. first is a distance function
var distance=function(p1,p2) {
// pythagorean theorem for distance between two points
return Math.sqrt(Math.pow(p2[0]-p1[0],2)+Math.pow(p2[1]-p2[1],2));
},
// function to determine how long a move chain was
moveDistance=function(lastMove){
return _.chain(lastMove.split(/,/)) //< break the move string up by commas
.map(parseFloat) //< turn each thing into a number
.groupBy(function(v,k){ //< break up the array into points
return Math.round((k-1)/2);
})
.reduce(function(m,p){ //< walk the points adding the distance between them.
if(m.pp){
m.sum+=distance(m.pp,p);
}
m.pp=p;
return m;
},{
pp:null,
sum:0
})
.value()
.sum; //< return just the sum distance
};
// run this function when graphics (tokens) are changed
on('change:graphic',function(obj,prev){
// setup some variables for later use
var page, move, theta,tprime,lprime;
// determine if we want to affect this object
if( 'objects' === obj.get('layer') /* on the objects layer */
&& -1 !== obj.get('statusmarkers').indexOf('overdrive') /* has the overdrive symbol */
&& (obj.get('top') !== prev.top || obj.get('left') !== prev.left) /* has changed position */
) {
// grab the page object based on the page the token is on (for later use bounding)
page=getObj('page',obj.get('pageid'));
// figure out how far the token moved
move = moveDistance(obj.get('lastmove'));
if(!move){
// if it was just dragged directly, the lastmove field won't give us a distance,
// use the distance between where it is now and where it was instead
move=distance([obj.get('left'),obj.get('top')],[prev.left,prev.top]);
}
// cut move in half. move/=2 is the same as move = move/2
move/=2;
// get a random angle in radians
theta=(randomInteger(360) * (Math.PI/180));
// calculate the new top, bounded by half a unit from the top and bottom edge of the map
tprime = Math.max(35,Math.min((page.get('height')*70)-35,obj.get('top')+move*Math.sin(theta)));
// calculate the new left, bounded by half a unit from the left and right edge of the map
lprime = Math.max(35,Math.min((page.get('width')*70)-35,obj.get('left')+move*Math.cos(theta)));
// set the object's new position
obj.set({
top: tprime,
left: lprime
});
}
});
});