Advertisement Create a free account

Query regarding page settings.

Hello, my apologies if this has already been identified by my research is coming up dry.  Wondering if it's possible to create a script that would modify the current page's Custom unit, to a new name - in addition to changing the page scale.  Many thanks.  Reasoning for this is to generate a travel cost calculator using the measure ruler. Reasoning for preference to a Ruler is the way point feature which is helpful for river travel etc where the distance between a token is not the best measurement. Granted it would be possible to just have the basic page setup like this but would be nice to have a quick option to change values based on land, river cost in addition to back to basic measurement utility Perhaps a bit of a dream :D
1557922009
The Aaron
Forum Champion
API Scripter
The units are available on the page object in the "scale_units" field: Figuring out what the "current" page is can be a bit difficult, here are a couple of functions I wrote that will do that for you: const getActivePages = () => [...new Set([ Campaign().get('playerpageid'), ...Object.values(Campaign().get('playerspecificpages')), ...findObjs({ type: 'player', online: true }) .filter((p)=>playerIsGM(p.id)) .map((p)=>p.get('lastpage')) ]) ]; const getPageForPlayer = (playerid) => { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; getPageForPlayer(playerid) will give you the pageid for the page a player is most likely to be on. It can only be wrong in the case of a GM, and only if they have multiple browsers open and logged into the same game and have switched pages in one browser window, but are interacting in a different browser window.  (That might be confusing, but if you're not in multiple browser windows on the same game, you won't run into it.)
A wizard appears! - Many thanks for the input, sounds like it might be possible to achieve what i'm hoping for, which will eventually be a few quick commands to swap the page measurements between various custom values - So that I can basically convert the ruler into a travel cost estimator (making use of waypoints for bendy river travel) - Likely pointless but I feel the craving to work on something pointless :D Though, far far out of my depth perhaps it's a little project for me to work on and an opportunity to learn some script at the same time.
1557942153
The Aaron
Forum Champion
API Scripter
I think it's a fantastic first project, and I'm happy to help you at any level or step along the way, just ask!
The Aaron said: I think it's a fantastic first project, and I'm happy to help you at any level or step along the way, just ask! Righto, i'll take you up on that :D as someone who's only got some basic knowledge of macro's and zilch on coding. Where is the best place to start :D Happy to dabble a few hours here and there to learn the basics.
1557960192
The Aaron
Forum Champion
API Scripter
I suggest finding Javascript: the Good Parts by Douglas Crockford. It's an older book, but it's a good grounding on javascript. Then I'd grab some simple API scripts and make some minor mods to them just to get the feeling of the API. I'll post a few to look at in a few minutes. 
1557961300
The Aaron
Forum Champion
API Scripter
Here is a script that causes any token with the Overdrive symbol (a swirly vortex) to be moved a random direction whenever they are moved, rebounding off walls.  It's well annotated: // 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 }); } }); });
1557961360
The Aaron
Forum Champion
API Scripter
Here's one that spawns a random FX on each selected token when you run the command !randomfx&nbsp; (also well annotated): // 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={}, //&lt; an object to store tokens that are in the rotation to receive random animations minSeconds = 5, //&lt; the minimum time to wait before applying another effect maxSeconds = 15, //&lt; 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 &amp;&amp; playerIsGM(msg.playerid) &amp;&amp; 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: &lt;typename&gt; ,_id: &lt;id&gt; } 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 -&gt; .map() -&gt; 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 -&gt; .compact() -&gt; 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 -&gt; .each() -&gt; 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 &lt;object&gt;.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 &lt;object&gt;[&lt;property name&gt;] 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'))); //&lt; 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')
1557964671
The Aaron
Forum Champion
API Scripter
This is a pretty fun Javascript tutorial:&nbsp; <a href="http://jsforcats.com/" rel="nofollow">http://jsforcats.com/</a>
1557964839
The Aaron
Forum Champion
API Scripter
And this looks like a pretty comprehensive modern javascript tutorial:&nbsp; <a href="https://javascript.info/" rel="nofollow">https://javascript.info/</a>