Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Script Request] Activate fx Effect Randomly on Selected Tokens

1458517981

Edited 1458518019
I have created a custom fx, but I think I need an API script to use it how I am envisioning it. This is for visual effect only, so there's no interaction with character stats. Only specified tokens on the map are to be affected. Maybe about 6 tokens total. I want the fx effect to randomly activate on a token every 5 to 15 seconds. Each token's activation is independent of the other tokens, so in any particular second, 0 to 6 of the tokens will be activated. I want this script to continue until I end it, though it won't need to persist beyond one map. I hope that's enough to get across what I'm asking for. Have I missed some way to do this via macros instead? Is there a script that can do this currently? Is this even something an API script can do? Would running continuously put too much overhead on things? It does not have to be truly random. A long enough loop would work so long as no pattern is easily discernible to players. Any help greatly appreciated.
Not possible using macros but should be possible via the api. 
1459214305

Edited 1459214357
I am so lost with javascript, I think I will have to fudge the random part of this. Is something like the following pseudocode doable with the API? listen for chat command !mycustomfx to launch i=0 while i<100     sendChat(/fx effect-color token_id1)     sleep 2000ms     sendChat(/fx effect-color token_id2)     sendChat(/fx effect-color token_id3)     sleep 3000ms     sendChat(/fx effect-color token_id1)     sendChat(/fx effect-color token_id4)     sleep 3500ms     sendChat(/fx effect-color token_id2)     sendChat(/fx effect-color token_id1)     sendChat(/fx effect-color token_id5)     sendChat(/fx effect-color token_id6)     sleep 2000ms     sendChat(/fx effect-color token_id2)     sendChat(/fx effect-color token_id5)     sleep 3000ms     sendChat(/fx effect-color token_id3)     sleep 1000ms     sendChat(/fx effect-color token_id4)     sendChat(/fx effect-color token_id6)     sleep 2500ms     sendChat(/fx effect-color token_id1)     sendChat(/fx effect-color token_id3)          i++     if !exit command has been entered in chat then exit script end while loop exit sleep function defined here
1459255072

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

Edited 1460344550
Just wanted to post another thank you for the script. Just finished the session where I used it. My players were suitably impressed, and I could not have made that impression without your scriptomancy, Aaron. I modified the script to a set effect, but kept the random timing. Here's a bit of how it looked (click the image to see it in action):
1460347471
The Aaron
Pro
API Scripter
That's pretty spectacular! &nbsp;As always, I'm happy to help! &nbsp;=D Happy rolling!
1464913116
James W.
Sheet Author
API Scripter
I thought I'd also chime in with a thanks; I adapted it so I could use it for a visual representation of the sparks coming off of a construct before it explodes in the module I'm running tomorrow.
1464914172
The Aaron
Pro
API Scripter
Very awesome! &nbsp;Are you streaming it?