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!
August 12 (8 years ago)

Edited October 09 (7 years ago)
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.

August 13 (8 years ago)

Edited August 13 (8 years ago)
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.
August 13 (8 years ago)
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.
August 13 (8 years ago)
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
August 13 (8 years ago)
The Aaron
Pro
API Scripter
Ah, sorry about that. Typo. :)

Really, you can just use 'let' and everything will be fine. 
August 13 (8 years ago)
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?
August 13 (8 years ago)

Edited August 13 (7 years ago)
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. 
August 13 (7 years ago)

Edited August 14 (7 years ago)
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)
August 14 (7 years ago)

Edited August 14 (7 years ago)
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.
August 14 (7 years ago)

Edited August 14 (7 years ago)
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
August 14 (7 years ago)
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.
August 14 (7 years ago)
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
August 14 (7 years ago)
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.
August 14 (7 years ago)

Edited August 14 (7 years ago)
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.
August 15 (7 years ago)
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). 
August 15 (7 years ago)
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. 
August 16 (7 years ago)
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?
August 21 (7 years ago)
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.


August 22 (7 years ago)
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.

August 22 (7 years ago)
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.
August 23 (7 years ago)
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?
August 28 (7 years ago)
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.
August 28 (7 years ago)
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?
August 30 (7 years ago)
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!

August 30 (7 years ago)
The Aaron
Pro
API Scripter
Ah, good.  It seems like the order of execution for scripts is thus:
  1. Load the state object
  2. Execute each script
  3. Load all objects of the game
  4. Fire the 'ready' event
  5. 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.
September 02 (7 years ago)
The script is getting hung up on the handout.get - it says it is not a function. Trying to figure out what's wrong...
September 02 (7 years ago)

Edited September 02 (7 years ago)
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.
September 02 (7 years ago)
The Aaron
Pro
API Scripter
Ha!  That's what I get for typing it out from memory on my phone...
September 18 (7 years ago)
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]]')
September 18 (7 years ago)
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
September 18 (7 years ago)
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:  https://regex101.com/r/cW3yrj/2


September 20 (7 years ago)
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.
September 28 (7 years ago)
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?
September 28 (7 years ago)
The Aaron
Pro
API Scripter
You will need a token to set it from.  If you don't have one on the map already, you'll have to create one with createObj().  Then you can use setDefaultTokenForCharacter() to set the default token.  
October 04 (7 years ago)
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.

https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164

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.
October 04 (7 years ago)
The Aaron
Pro
API Scripter
This is a common enough problem that I've got a function for it in the Wiki: https://wiki.roll20.net/API:Cookbook#getCleanImgsr...
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.  It handles some other problems (like a missing cache buster value) and will rename to use the thumb endpoint.
October 05 (7 years ago)
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."

    function createToken(sheet) {
        let currentPageID = Campaign().get('playerpageid');
        let cleanimg = getCleanImgsrc("https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164");
        createObj("graphic", {
            name: "monstertoken",
            subtype: "token",
            layer: "objects",
            imgsrc: cleanimg,
            pageid: currentPageID,
            bar1_link: "hp",
            bar2_link: "init",
            bar3_link: "speed",
            controlledby: sheet.id
        });
    }
    
    function getCleanImgsrc(imgsrc) {
       let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/);
       if(parts) {
          let retval = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
          sendChat('',`img = ${retval}`);
          return retval;
       }
       return;
    }; 
This is the path that gets spit out by the sendChat, which seems to work fine if I plug it directly into the browser:
https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/thumb.png?1502085164


I don't really understand the purpose of the Math.random but it doesn't seem to be doing anything to this path.
October 05 (7 years ago)
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.  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).  I stripped this down to a bare minimum to test with and this created the token for me:
on('ready',()=>{
    function createToken() {
        let currentPageID = Campaign().get('playerpageid');
        let cleanimg = getCleanImgsrc("https://s3.amazonaws.com/files.d20.io/images/37204591/vJHd7d2t51hPCssN3O3bjw/max.png?1502085164");
        createObj("graphic", {
            name: "monstertoken",
            subtype: "token",
            layer: "objects",
            left: 35,
            top: 35,
            width:70,
            height:70,
            imgsrc: cleanimg,
            pageid: currentPageID
        });
    }
    
    function getCleanImgsrc(imgsrc) {
       let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/);
       if(parts) {
          let retval = parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
          return retval;
       }
       return;
    }
    
    createToken();
});

The Math.random() is there to create the ?########### on the end of the URL if it's missing.  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.