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

[Help] Just starting to learn the API - need help getting started

I just need a little push to help me get started writing a script and I'm just having trouble finding just the few tidbits I need. All the examples, scripts and tutorials I'm finding online quickly get over my head without ever telling me clearly the few basic things I'm trying to do. Could someone write a simple script that does the following for me so I can see how these basic mechanisms work: Receive a command from the chat window to run the script (lets say I type "getfruit" in the chat window or "!getfruit" or however commands work) Roll a D6 (let's say it results in a value of 3) Use that D6 result as an index reference to an array with 6 values in it (fruit = ["apple","pear","peach","mango","pineapple","plum"] so a value of 3 on the dice results in an index lookup of fruit[2] assuming indexes go from 0-5 in this case) Return that item to the chat window. (it returns fruit[2] which is "peach") So, I type !getfruit and it rolls a 3 and returns "peach" in the chat window. What I've pieced together so far is: /* globals sendChat, randomInteger, _, on */ var getfruit = (function() { 'use strict'; if (msg.type === "api" && msg.content.indexOf("!getfruit") !== -1){ var fruit = ["apple","pear","peach","mango","pineapple","plum"]; sendChat('gm',fruit[randomInteger(6)-1]); } }); Obviously it doesn't work but in the hours I've searched I can't find the pieces I'm missing in an easy to understand tutorial or code example. Please help!
1502558023

Edited 1507561393
The Aaron
Pro
API Scripter
You're not far off.  Here's how I'd write that: on('ready',()=>{     const fruit = ["apple","pear","peach","mango","pineapple","plum"];          on('chat:message',(msg)=>{         if('api'===msg.type && msg.content.match(/^!getfruit/)){             sendChat('Your Fruit',`Your fruit is: ${fruit[(randomInteger(6)-1)]}`);         }     }); }); What you're missing is that the API is  event driven .  You have to register functions to be called when particular events occur.  In the above, I'm registering for 2 events.  The first one is the 'ready' event, which happens once the API is fully spun up.  It's usually best to delay real work until 'ready' happens.  The second event is the 'chat:message' event, which occurs whenever someone types in the chat box and hits enter. BTW, the first parameter to sendChat() is "who's talking".  If you want a chat message to go to the GM or a particular person, you'd put "/w gm " in front the same way you'd do in chat.
1502585657

Edited 1502585895
Thank you very much! That script and your explanation helps tremendously. I have a few minor followup questions: What is the "()=>" part of the on ready event? Is that just a shorter replacement for "function()" that I see in most scripts? Why is it const instead of var? Do I have to import some sort of globals to utilize the randomInteger function? If not, is there a list somewhere of the available global functions we have access to?  Never mind this last one, I found it.
1502586864
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Todd B. said: Thank you very much! That script and your explanation helps tremendously. I have a few minor followup questions: What is the "()=>" part of the on ready event? Is that just a shorter replacement for "function()" that I see in most scripts? Why is it const instead of var? Do I have to import some sort of globals to utilize the randomInteger function? If not, is there a list somewhere of the available global functions we have access to?  Never mind this last one, I found it. Yep, ()=> is a shorter form for function() {}, it also doesn't create a new scope (Aaron will have to explain this in more detail). I'd guess that Aaron used const because nothing is manipulating the fruit, so it doesn't need to be able to change via push, shift, splice, or any of the other array manipulation functions.
Ok, so const is this language's version of an immutable object? If I want it to be mutable then I'd use var I assume.
1502590701
The Aaron
Pro
API Scripter
Yeah, basically. Modern JavaScript has 3 ways to declare a variable: var -- this is the old way, it creates a mutable variable at function scope which is hoisted to the top of that scope.  let -- this creates a mutable variable at block scope with no hoisting.  const -- this creates an immutable variable at block scope with no hoisting.  In this case, immutable means you can't reassign the variable, but doesn't mean the variable's contents won't change. So a const variable that points to an object will always point to that object, but the object's properties might be changed (so you might append more items to the array, but it will always be an array, and will always be THAT array, even if the contents of that array change.  Function scope means inside that function.  Block scope means inside the { } block.  Hoisting means that if you use var to create a variable on the last line of your function, it's definition is hoisted to the top of the function scope and is defined for the entire body of the function.  let and const were introduced as part of ES6 to address the fact that var behaves so strangely compared to just about any other c-style language. let and const pretty much behave as you would expect, they are defined in the tightest scope that contains them, from the line where they are declared on, and undefined before that point or in a scope wider than their own.  As Scott said, () => {} is a nice shorthand way of creating functions. For the most part, you can just treat them like the long form function() {}. The only real difference is that they only have a block scope and not a function scope. You're unlikely to ever run into the importance of that distinction. =D. You can read up on "fat arrow functions" if you're curious (or maybe entice Brian to give a discussion on it. ( you know you want to Brian! =D ) ). Anyway, common wisdom on writing modern JavaScript is to use const by default, and let if you must, and never use var. Also fat arrow functions are very much in vogue. =D
Ah, ok. It makes sense to use const if you can change the data inside the variable, though I'm afraid a lot of everything else you said is a little over my head and makes me dizzy - I only know a bit of python, and I'm a novice at best at even that. I kind of get the gist of what you're talking about though, it's just going to be a while before I can wrap my head around it and write efficient scripts. By the way, playing around with the script above, I found that msg.context.match didn't work, but msg.content.match fixed it. I saw something somewhere about weighted die rolls or weighted table lookups or something, I'm trying to figure that out now
1502594305
The Aaron
Pro
API Scripter
Ah, sorry about that. Typo. :) Really, you can just use 'let' and everything will be fine. 
1502594374
The Aaron
Pro
API Scripter
If you're interested, I can get you some examples and more thorough explanations when I'm not on my phone. 
I saw in several other scripts that the on-ready call is at the end of the script with just a call to a registerEventHandler function in the main function above it, which in turn calls a handleInput function which does the necessary chat stuff. I'm trying to plug in that methodology into my script but I am getting a "registerEventHandler is not a function" error and I'm not sure why. There may very well be other issues with the script that I'll run into but that one is currently stumping me. Here's where I'm at with my script so far: var gettreasure = (function() { const twodsix = (function() { return randomInteger(6)+randomInteger(6); }); const dtwenty = (function() { return randomInteger(20); }); const valuables = [ {low: 1, high: 10, result: ["all copper", "all silver", "in foreign currency", "in local currency", "in a sack", "in small pouches", "in a large pouch", "in a bowl", "in a box", "in an unlocked chest", "in a locked chest"]}, {low: 11, high: 13, result: ["Ruby", "Emerald", "Opal", "Aquamarine", "Topaz", "Garnet", "Amber", "Pearl", "Amethyst", "Diamond", "Sapphire"]}, {low: 14, high: 16, result: ["Circlet", "Pendant", "Badge", "Brooch", "Bracelet", "Ring", "Necklace", "Earring", "Clasp (Fibulae)", "Arm Ring", "Crown"]}, {low: 17, high: 18, result: ["Scabbard Loop", "Hilt Plate", "Fittings, Studs", "Buckle & Plate", "Strap Fitting", "Pommel Cap", "Horse Trapping", "Scabbard Trapping", "Seal", "Beads", "2d6 Mineral (ingots)"]}, {low: 19, high: 20, result: ["Comb", "Talisman", "Animal Statuette", "Bowl", "Goblet", "Tableware", "Jug", "Cup", "Lamp", "Candlestick", "Censer"]} ]; function _getValuables(roll) { return _.find(valuables, function (test) { return (roll >= test.low && roll <= test.high); }); } function registerEventHandlers() { on('chat:message', handleInput); } function handleInput(msg) { if('api'===msg.type && msg.content.match(/^!gettreasure/)) { const treasure = gettreasure; const row = twodsix; const col = dtwenty; const chart = treasure._getValuables(col); sendChat('/w gm Treasure:',`${chart[row-1]}`); } } return { registerEventHandlers: registerEventHandlers }; }); on('ready', function() { 'use strict'; gettreasure.registerEventHandlers(); });
Also, how do you paste in code into these posts like you've done above?
1502609598

Edited 1502633054
The Aaron
Pro
API Scripter
Yeah, that's how I like to organize my larger scripts. It's called the  Revealing Module Pattern .  What you're missing is the IIFE ("Iffy" -- Immediately Invoked Function Expression). You've fallen prey to a bit of cargo cult programming with all that wrapping functions in () =D. You only need to wrap the outer one (and even then it's more about convention). The basic layout is this: const module = (function(){ /* create things in function scope */ return { something: funcScopeThing }; })(); // note that this is executing module.something(); so module gets assigned the result of executing the anonymous function, an object with the property something.  This returned-out-of-function-scope thing is called a Closure. The thing returned (aliased as module.something) retains access to the things created in the function scope. It's a way to have an internal private implementation in a language that is generally public everything.  Code formatting is available in the paragraph symbol at the top left. 
1502634806

Edited 1502717743
The Aaron
Pro
API Scripter
Minor correction, it's a lexical scope for 'this' that fat arrow functions lack. They have a function scope. They can't be used with new to create prototypical inheritance objects like functions can, and they don't get the arguments variatic variadic magic object on execution. (But you can use the rest operator to get the same thing without the weird semantics)
1502685829

Edited 1502685891
I kind of understand what you're saying about IIFE, and that certainly pushed me in the right direction to get my script working (as of a few minutes ago at any rate - now to work out how to build the rest of it) , , , but you're going over my head again. ;)   There's a lot of programming terminology that I really just don't have a good grasp on what it all exactly means. Scope - I kinda have the gist of what this means, but no idea what you mean by "lexical scope." Fat Arrow Functions - No idea yet what that is, and haven't had time to try looking it up yet. Inheritance Objects - I have a vague notion of what this means, I don't know what prototypical inheritance objects means. Variatic Magic Object - No idea what this is. Rest Operator - I don't know what that is yet, or the whats or whys it is used You don't need to explain all this stuff, you've already been a tremendous help and I don't want to waste your time with newbie questions. I will probably eventually get to the point of understanding it all. My next goal is understanding the formatting for if-then, for and while loops, as well as manipulating arrays as I'm going to be using them quite a bit for this script.
1502717516

Edited 1502717563
The Aaron
Pro
API Scripter
Scope is the part of the program where something is accessible: // global scope function test(){ // function scope if(true){ // block scope } } Declaring a variable with var is hoisted to the current function or global scope, meaning this: function test(){ if(true){ var hoistedFunctionScope = 42; } } is treated like this: function test(){ var hoistedFunctionScope; if(true){ hoistedFunctionScope = 42; } } It can lead to issues, particularly with the looping constructs.  Generally it only causes problems with sloppy code, but const and let prevent the problems because they aren't hoisted and have block scope: function test(){ if(true){ let blockScoped = 42; } } Fat Arrow Functions is just what they call functions declared with the => like this: const square = (x) => x * x; as opposed to traditional function declarations like these: function square(x){ return x * x ; } const square = function(x){ return x * x ; } Inheritance Objects -- I worded that poorly.  I should have said "they can't be used to create new objects by passing them as the argument to the new operator."  Javascript uses a somewhat bizarre method for object-oriented inheritance called prototypical inheritance.  You can mostly ignore it but the gist of it is "you create copies of a prototype object.  Changes to that prototype object at runtime are inherited by all the copies."  To create new objects, you pass the prototype function to the new operator: const Proto=function(){ this.thing=3; } let obj = new Proto(); Fat Arrow Functions can't be used like that because they don't have a this object and the wiring that goes along with it.  It's probably not important to understand that as in modern Javascript you're creating classes and making instances of those for all your object-oriented needs. Variadic Magic Object -- to support writing a function that takes a variable number of arguments, javascript has a magic object that functions get named arguments : const sum = function(){ let tot=0; for(let i = 0; i<arguments.length; ++i){ tot+=arguments[i]; } return tot; }; const total = sum(1,2,3,4,5); // total is 15 Fat Arrow Functions don't get that arguments object.  The arguments object is almost an Array, but not quite, which can be confusing.  However, in modern Javascript, you have the Rest Operator which can accomplish the same thing (this might not be available on the API yet): const sum = ( ...args )=>args.reduce((m,a)=>m+a,0); The rest operator has the benefit that it actually produces an Array object, so everything works as expected.  You can also combine it with other named arguments: const something = (named1, named2, ...theRest) => /* some code */; It has a counterpart operator called the Spread Operator , which spreads all the properties or values of an object or array into the context of another object or array (also possibly not in the API yet): const mergeObjs = (obj1, obj2) => ({ ...obj1, ...obj2}); The nice thing is, you probably don't need to understand any of that to get some great benefit from the API and even to write some really useful scripts.  But if it interests you, I don't mind droning on about it. =D  Talking about it reinforces my understanding and if I got anything wrong, Brian will likely correct me. =D
1502723783
Jakob
Sheet Author
API Scripter
Small addition to The Aaron's explanation: all the things which have been stated as possibly not being in the API are, in fact, in the API, it has been updated to Node v7.x a few months ago.
1502725320
The Aaron
Pro
API Scripter
SWEET!  Thanks for verifying that Jakob.  I looooove Rest and Spread.  Using the crap out of them in work code... =D
1502730733
Lithl
Pro
Sheet Author
API Scripter
Scott C. said: I'd guess that Aaron used const because nothing is manipulating the fruit, so it doesn't need to be able to change via push, shift, splice, or any of the other array manipulation functions. const only prevents reassignment. It doesn't make the object immutable. If you have a const array, you can still push, shift, splice, etc. as you please. In order to prevent manipulation, you need Object.freeze(myObj). const constantArr = []; constantArr.push(5); console.log(constantArr); // [5] constantArr = [5, 6]; // TypeError: Assignment to constant variable let frozenArr = Object.freeze([]); frozenArr.push(5); // TypeError: Cannot add property 0, object is not extensible frozenArr = [5, 6]; console.log(frozenArr); // [5, 6] const constantFrozenArr = Object.freeze([]); constantFrozenArr.push(5); // TypeError: Cannot add property 0, object is not extensible constantFrozenArr = [5, 6]; // TypeError: Assignment to constant variable There is also Object.seal, which behaves similarly, except that freeze also makes existing properties immutable, while seal does not.
1502731416

Edited 1502731432
The Aaron
Pro
API Scripter
Probably I should start using a different term than immutable for const.  The variable is immutable, though the value is not, but that's probably too fine a distinction.  Similar to the difference between a const pointer and regular pointer in C++. =D
Thanks!  I understood a lot of that because of your excellent explanation. Understanding and knowing when best to use them is a whole other ball of wax. I'm not sure why I'd ever prevent manipulation of a variable unless there was some sort of security concern or it's more memory efficient for instance. Now I'm trying to figure out how best to import the information my script needs from the !message. I tried to do: const mymessage = msg.content; and then const mymessage = msg.content(); But neither one worked - it complained about content not being a function. I then did: const mymessage = msg.content.replace("!script ",""); Which did give me basically what I needed and can work for my purposes, but what function after msg.content do I use to just pass in the message into the variable as a string?  I feel like it shouldn't be that hard to find but I can't find any list of the available functions.
1502773012
The Aaron
Pro
API Scripter
msg.content is a string. I like to use: let cmds=msg.content.split(/\s+/); that makes cmds an array of all the strings in the command, split by white space. You can then use cmds.shift() to remove the command (and possibly switch on it). 
1502775710
The Aaron
Pro
API Scripter
Most of using const and Object.freeze() and the like are more about preventing accidental  errors than malicious ones. It lets the interpreter tell you when you typed the wrong name. Additionally, it can make things faster in a few ways. The interpreter is free to do more optimization behind the scenes if it knows you won't be changing a variable. But you can take advantage as well. If you know a variable can't be reassigned and you know you've assigned it a value, you can avoid the if checks to verify it. 
1502913087
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: msg.content is a string. I like to use: let cmds=msg.content.split(/\s+/); that makes cmds an array of all the strings in the command, split by white space. You can then use cmds.shift() to remove the command (and possibly switch on it).  [shameless plug] I'm a fan of  splitArgs : let args = msg.content.splitArgs(); let command = args.shift().substring(1); The splitArgs script will let you handle quoted arguments (eg, !mycommand foo "bar baz"  will treat bar baz as one argument, instead of having the arguments "bar  and baz" ). You can also pass a different argument delimiter to the function, either a string or regular expression (/\s/g is used by default). It also works with single quotes ( !mycommand foo 'bar baz'  is the same as the above example), and doesn't get confused by a single level of nested quotes of the opposite type ( !mycommand "this arg 'has nested' quotes"  results in the single argument this arg 'has nested' quotes ). splitArgs is also available as a one-click install. [/shameless plug]
Alright! I finally got my entire script working beautifully thanks to all the help but now I've run into another wall. How do I execute an api script from a macro? This is the function (and the data I'm passing it) which I'm trying to call within a macro and which draws upon attributes of the creature in question: !gettreasure @{selected|token_name}|@{Level}|@{Ind Treasure Dice}|@{Ind Treasure Odds}|@{Lair Treasure Dice}|@{Lair Treasure Odds} The result would look like this in the chat: !gettreasure Skeleton, Decrepit|1|1|100000|1|200000 Trying to spit out the command into the chat doesn't work (it just doesn't spit anything out) and I only found one post in the forums that mentions it isn't possible without some sort of interception script. If that's the case, that seems like a pretty bit oversight in functionality. How would I go about getting this to work?
1503322654
The Aaron
Pro
API Scripter
Hi! Sorry for the delay getting back here, was at GenCon the last 4 days, and then sleeping off GenCon for 14 hours or so.. =D First, many people mean different things when they say "from a macro."  From a literal Macro on the Collections Tab, something you can toggle onto the macro button at the bottom of the VTT, below the player names, or call from the chat with a #<name>.  You should just be able to put your command in there and run it, with the caveat that all the attribute references will need to be explicitly targeted with @{selected}, @{target} or @{<Character Name>}.  If everything is on the character of the selected token, you'd just do this: !gettreasure @{selected|token_name}|@{selected|Level}|@{selected|Ind Treasure Dice}|@{selected|Ind Treasure Odds}|@{selected|Lair Treasure Dice}|@{selected|Lair Treasure Odds} That will probably sort it out for you. If you're talking about an Ability on the character, which you add on the Attributes and Abilities tab and can toggle to shows up in the Token Action bar, or type in the chat with a %<name>, then you could use what you had above. You could use this small script to monitor what is coming to the chat: on('chat:message',(msg)=>log(msg)); That might reveal what the issue is.  For API commands, the ! must be the first character on the line.  (Some scripts which are simplistically written will detect commands elsewhere in the line, but the type of the message is not an 'api' message and they shouldn't activate.)  That's where I'd start with debugging. If you don't find it in a reasonable amount of time (your choice on what that means!) feel free to PM me a join link and GM me and I'll be happy to come try and find it with you.
No worries about the delay - I was just now getting back to seeing if I could figure it out. I didn't realize I needed to put @{selected|...} in front of every attribute - that did the trick and it works perfectly now. Thanks yet again! Yes, what I meant by macro was the little scripts you could put in the abilities of a character or monster, which can show up on its macro bar when its token is selected. Now that treasure generation is working for the monsters, I have to figure out how to import monsters into roll20 and get their stats to auto-populate into their appropriate attributes. I'm really not even sure where to start yet. It would be awesome if roll20 had some way of accessing documents on google drive, then it'd just be a simple matter of grabbing the necessary document and parsing the data.
1503388139
Jakob
Sheet Author
API Scripter
Todd B. said: Now that treasure generation is working for the monsters, I have to figure out how to import monsters into roll20 and get their stats to auto-populate into their appropriate attributes. I'm really not even sure where to start yet. It would be awesome if roll20 had some way of accessing documents on google drive, then it'd just be a simple matter of grabbing the necessary document and parsing the data. Note that if you're playing 5E and using the Shaped sheet, the work has mostly been done for you if you can get your monster data into JSON format... - if you're not, this is a pretty hard problem.
1503410979
The Aaron
Pro
API Scripter
Also, the Shaped Companion will still import from OCR'd monster stat blocks in a token's GM notes... and does a pretty good job of it!
No, this is all for my own game system I've been developing for some time. The approach I'm considering is writing a script in Google Drive to convert the monster data (which is in a spreadsheet) into a text format, then just copy/paste that text into a campaign note, then have an API script grab the data from the note to populate a monster attributes in a template. A little brute-forced but it'll accomplish my short term goals. If I could figure out a way for an API script to read in multiple blocks of text and create multiple monsters all at the same time that would be ideal.
1503521801
The Aaron
Pro
API Scripter
You can certainly do that.  It's just a matter of setting up separators correctly. ###BEGIN NEW MONSTER### name: Monster A rank: 1st order serial number: 1234 ###BEGIN NEW MONSTER### name: Monster B rank: 3rd order serial number: 8675309 That makes it pretty easy to handle... I actually have an unreleased script that exports an entire character, including attributes and abilities in a format similar to that, then will update the same creature from that format (or create a new one).
Got the script to parse the spreadsheet and spit out a formatted text block working. I think I'm getting a little better with javascript now. Next step, write a script to convert that text block into a monster in roll20. I assume there's a way to create a character sheet/monster sheet within a subfolder in the Journal?
1503903457
Jakob
Sheet Author
API Scripter
Todd B. said: Got the script to parse the spreadsheet and spit out a formatted text block working. I think I'm getting a little better with javascript now. Next step, write a script to convert that text block into a monster in roll20. I assume there's a way to create a character sheet/monster sheet within a subfolder in the Journal? No, the folder structure is not accessible to the API.
1503919237
The Aaron
Pro
API Scripter
Slight clarification: the API can read the folder structure, but had no capability to affect it, including no capability to choose the location a journal entry is created. 
Ugh. That's quite the organizational oversight. This process just got a heck of a lot more laborious. :(
I'm stuck again, and I'm just not finding a good example. I have a handout with a bunch of text in its notes section. How do I get that text into a variable in the api and then have it print out that variable in the chat? I've tried: var rawdata = getObj("MonsterImport"); return rawdata.get("notes"); and var character = findObjs({ type: 'handout', name: 'MonsterImport' })[0]; character.get("notes", function(notes) { log(notes); //do something with the character bio here. }); and var character = getObj("handout", "-JMGkBaMgMWiQdNDwjjS"); character.get("notes", function(notes) { log(notes); }); I think all of these error out saying "can not read property 'get' of undefined". There's no documentation that offers an easy to understand example, and so at this point I have no idea how to proceed. Help?
1504097797
The Aaron
Pro
API Scripter
My speculation is that you aren't getting an object back.  Are you running these inside an on('ready',...) ?  Try this: on('ready',()=>{ const handout = findObjs({ type: 'handout', name: 'MonsterImport' }, {caseInsensitive: true} ); if(handout){ handout.get('notes',(n)=>{ log(`MonsterImport.notes: ${n}`); }); } else { log(`ERROR: No handout named 'MonsterImport' found.`); } });
Oh, that's probably it. I just started a new script to test functions out and forgot about the on ready - I'll try that tonight. Thanks!
1504114329
The Aaron
Pro
API Scripter
Ah, good.  It seems like the order of execution for scripts is thus: Load the state object Execute each script Load all objects of the game Fire the 'ready' event React to events. There are rare cases when you want to be notified of every event on creation of objects, but usually you'd rather start when everything is there.
The script is getting hung up on the handout.get - it says it is not a function. Trying to figure out what's wrong...
1504339143

Edited 1504339186
Jakob
Sheet Author
API Scripter
Put a [0] behind the findObjs() call, as you correctly did in your own script. findObjs returns an array. I'm disappointed, The Aaron :D.
1504339372
The Aaron
Pro
API Scripter
Ha!  That's what I get for typing it out from memory on my phone...
I've been hammering away at this and so far so good, a few stumbles but I eventually figured out how to fix the problems I've run into...slowly... Parsing the data has been very tricky, but I'm getting closer to the end. I'm at the point where I'm trying to auto-generate the macro string from the data it's parsing and I've got the attack part worked out and now I'm trying to figure out the best way to go about parsing the damage data. So one of the more complex damage strings read from the original spreadsheet might be something like this: "2d6+5 P1 Damage, Body save or take 1d6 Fire Damage" The idea is that the 2d6+5 becomes [[2d6+5]] and the 1d6 becomes [[1d6]] Is it possible to write a regex search and replace that finds a cluster like 2d6 and then puts double brackets around it? Maybe something like: string.replace(/\dd\d+\d/,'[[\dd\d+\d]]')
1505730993
Jakob
Sheet Author
API Scripter
Yes, you'd use something like this (important, 'g' flag to replace all matches): string = string.replace(/\dd\d+\d/g, '[[$&]]') Here, $& inserts the whole matched substring, so the whole dice expression. Though you'd need to use a more complex regular expression to get everything, maybe something like this as a start (this is probably incomplete): /\d+d\d+(?:\s*[+-]\s*\d+)?/g
1505737775
The Aaron
Pro
API Scripter
I'd suggest: /(?:\d+[dD]\d+|\d+\s*[*+-]\s*\d+[dD]\d+)(?:\s*[*+-]\s*(?:\d+[dD]\d+|\d+))*/g Here's a sandbox for it: &nbsp; <a href="https://regex101.com/r/cW3yrj/2" rel="nofollow">https://regex101.com/r/cW3yrj/2</a>
Thanks Jakob and Aaron, it's working like a charm so far. I had to study the regex to understand exactly how it is parsing - I'm slowly getting the hang of it. Next step: with the macro generation working, time to see if the code to generate the abilities on the monster sheet and plug the macros into them works...tomorrow.
So, I've got the script auto generating the monster sheet and populating it with attributes and macros - I still have some tweaks to do but it's working well. I'm now trying to figure out how to set up the default token, so that when you drag the sheet onto the map its token has its bar attributes already set. I haven't been able to find a way to do this in the api - how would I go about setting up a default token from within the api?
1506580502
The Aaron
Pro
API Scripter
You will need a token to set it from. &nbsp;If you don't have one on the map already, you'll have to create one with createObj(). &nbsp;Then you can use setDefaultTokenForCharacter() to set the default token. &nbsp; See:&nbsp; <a href="https://app.roll20.net/forum/permalink/3862777/" rel="nofollow">https://app.roll20.net/forum/permalink/3862777/</a>
I'm stuck trying to get the api to generate a token. "ERROR: You cannot set the imgsrc or avatar of an object unless you use an image that is in your Roll20 Library. See the API documentation for more info." So, I think I've figured out that the problem is that it wants a thumb version of the image (below), but the URL I'm getting from my image says "max.png". How do I get or create a thumbnail version that's named "thumb.png"? Just changing max.png to thumb.png still gives me the error message. <a href="https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164</a> By the way if any devs are looking at this, for usability sake, could you add in a right-click option when viewing and image in the library to "Display Info" which reveals information about the image such as pixel resolution, its URL, and whatever other data about the image? That would be extremely helpful - it seemed harder than it should be to get the URL of the image to plug into the api script.
1507125421
The Aaron
Pro
API Scripter
This is a common enough problem that I've got a function for it in the Wiki:&nbsp; <a href="https://wiki.roll20.net/API:Cookbook#getCleanImgsr" rel="nofollow">https://wiki.roll20.net/API:Cookbook#getCleanImgsr</a>... var getCleanImgsrc = function (imgsrc) { var parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; This will either return a URL that you can use in the API, or undefined if the image can't be used.&nbsp; It handles some other problems (like a missing cache buster value) and will rename to use the thumb endpoint.
Ok, I've tried plugging in that script and it's still erroring on me, though like when I changed the name to thumb.png, I got what is effectively an image not found error. Here's the relevant piece of code and the error I've got - am I still doing something wrong? "ERROR: You cannot set the imgsrc or avatar of an object unless you use an image that is in your Roll20 Library. See the API documentation for more info." &nbsp; &nbsp; function createToken(sheet) { &nbsp; &nbsp; &nbsp; &nbsp; let currentPageID = Campaign().get('playerpageid'); &nbsp; &nbsp; &nbsp; &nbsp; let cleanimg = getCleanImgsrc("<a href="https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164</a>"); &nbsp; &nbsp; &nbsp; &nbsp; createObj("graphic", { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: "monstertoken", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; subtype: "token", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; layer: "objects", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; imgsrc: cleanimg, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pageid: currentPageID, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bar1_link: "hp", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bar2_link: "init", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bar3_link: "speed", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controlledby: sheet.id &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; } &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; function getCleanImgsrc(imgsrc) { &nbsp; &nbsp; &nbsp; &nbsp;let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/); &nbsp; &nbsp; &nbsp; &nbsp;if(parts) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let retval = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('',`img = ${retval}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return retval; &nbsp; &nbsp; &nbsp; &nbsp;} &nbsp; &nbsp; &nbsp; &nbsp;return; &nbsp; &nbsp; };&nbsp; This is the path that gets spit out by the sendChat, which seems to work fine if I plug it directly into the browser: <a href="https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/thumb.png?1502085164" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/thumb.png?1502085164</a> I don't really understand the purpose of the Math.random but it doesn't seem to be doing anything to this path.
1507207225
The Aaron
Pro
API Scripter
I don't know why you'd be getting that error, unless it's coming from some other part of the script.&nbsp; That error message is particularly annoying because it will occur when you create otherwise valid objects, like a Roll Table Item or character, which can exist without an image. You are missing a location and size for the token (bold below) which would prevent you from seeing the token (it would be at 0,0 with a size of 0x0).&nbsp; I stripped this down to a bare minimum to test with and this created the token for me: on('ready',()=&gt;{ &nbsp; &nbsp; function createToken() { &nbsp; &nbsp; &nbsp; &nbsp; let currentPageID = Campaign().get('playerpageid'); &nbsp; &nbsp; &nbsp; &nbsp; let cleanimg = getCleanImgsrc("<a href="https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164</a>"); &nbsp; &nbsp; &nbsp; &nbsp; createObj("graphic", { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: "monstertoken", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; subtype: "token", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; layer: "objects", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; left: 35, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; top: 35, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width:70, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; height:70, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; imgsrc: cleanimg, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pageid: currentPageID &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; } &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; function getCleanImgsrc(imgsrc) { &nbsp; &nbsp; &nbsp; &nbsp;let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/); &nbsp; &nbsp; &nbsp; &nbsp;if(parts) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let retval = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return retval; &nbsp; &nbsp; &nbsp; &nbsp;} &nbsp; &nbsp; &nbsp; &nbsp;return; &nbsp; &nbsp; } &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; createToken(); }); The Math.random() is there to create the ?########### on the end of the URL if it's missing.&nbsp; I don't remember exactly what the circumstance is, but the is a way to end up with an image url without one and it caused some issues, so I just added it in there to fix them.