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

DeckBuilding API ?

I have made a TTRPG that uses TCG mechanics for combat. Me and my players have had a blast testing it and while Roll20's card/deck system is somewhat... basic. It's enough for us and my card system was built to be easy to use. We've played only one session, so I've added around 40 cards (Their deck + A few ennemies) manually, and that was fine. But now that I've added some deckbuilding elements (given them the freedom to choose which cards (and how many copies) they want in their deck.) I'm looking for an easier way than to have to do repetitive menial tasks. And hey, that's what a computer is good at right ? But before I start coding, I'd like to question my more experienced elders (that's you by the way) for some guidance. The API would do the following operations : 1. The API is given a list of card names and an amount (something like "2x BlueEyes; 1x MST; 4x BlackTerrain; 2x SmallKokiri") 2. The API creates or is given the name of an empty deck. 3. The API searches through a manually created Deck (A Card bank in which I've manually created one copy of each of these cards) and copies the cards to the empty deck based on the card list it was given in step 1. BONUS STEP. The API copies the card back of the Card bank and sets it for the new deck. Would these operation be possible with the current version of the API in your opinion ? Also, If you know of an API that already achieves something similar, I'd be pleased to hear about it. Thanks in advance.
1600187414
The Aaron
Roll20 Production Team
API Scripter
All of them are possible, to the best of my knowledge.&nbsp; There are some bugs with the API's access to card decks, but they're more around the dealing/stealing/playing functions.&nbsp; Deck creation should be good. Here's a very simple starter script that demonstrates creating a deck and putting a card in it: on('ready',()=&gt;{ let deck = findObjs({ type: 'deck', name: 'UserDeck' })[0] || createObj('deck',{ name: 'UserDeck', avatar: '<a href="https://s3.amazonaws.com/files.d20.io/images/85247185/lmStY4xKJkK8JaDKk-Km4w/max.jpg?1561855008" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/85247185/lmStY4xKJkK8JaDKk-Km4w/max.jpg?1561855008</a>' }); let card = findObjs({ type: 'card', deckid: deck.id })[0] || createObj('card',{ name: 'UserDeckCard', deckid: deck.id, avatar: '<a href="https://s3.amazonaws.com/files.d20.io/images/81257503/jWVnVUag6nVSsWYsJPw5-g/thumb.jpg?1557685980" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/81257503/jWVnVUag6nVSsWYsJPw5-g/thumb.jpg?1557685980</a>' }); }); The only problem would be if your images are not in a User Library, but are in the Marketplace.&nbsp; That sounds like it won't be an issue for you though.
Thanks for your answer and for the helpful example. I will then start the creation of my glorious DeckBuilding API, I'll make sure to make it available to the community if I succeed in my quest. It's been a bit of time since I fiddled with Javascript and the API but I think I'll manage. :D
1600187994
The Aaron
Roll20 Production Team
API Scripter
Definitely post back with question for the API, we love to answer them and help out!
My work has advanced quite a bit but I have ran into a problem. I use the following code in order to find the card I will end up copying. // Attempts to find the card. var cardToCopy = findObjs({ type: 'card', deckid: fromDeckId, name: cardName })[0]; I then tried to call the value of cardToCopy.name in order to get confirmation that they did indeed find the card. The value is always undefined. I've attempted multiple variations in order to debug the thing. But even something like : // Attempts to find the card. var cardToCopy = findObjs({ type: 'card' })[0]; End up giving me an undefined value. The game in which I am running this code does have decks, cards and no cards with no name. I'm at a loss on how to proceed. From what I've checked the deckId and name I was sending it were correct. EDIT : Here is my full code if needed&nbsp;<a href="https://pastebin.com/s0xxmUJV" rel="nofollow">https://pastebin.com/s0xxmUJV</a>
1600196760
The Aaron
Roll20 Production Team
API Scripter
Ah, this is a stumbling point for many a new API programmer.&nbsp; You must access properties for Roll20 Objects via the .get() function: let name = cardToCopy.get('name'); The Roll20 Objects are proxies for FireBase records, so they need to know when various things happen, like getting (to support lazy loading) and setting (to support synchronizing).&nbsp; The way they chose to do that was by exposing .get() and .set() methods. You might find these discussions informative: <a href="https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern" rel="nofollow">https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern</a> <a href="https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1</a> <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a>
This works. Magnificent ! Thanks for the documentation, I will consult it carefully.
My API script is complete. It's pretty rough but I'm sure someone can improve it for their own usage. I'll just leave it here for posterity. Deck names and card names must not contain spaces in order to make calls from the chat. var cardBankName = "CardBank"; // Change this to the name of the card bank var IsDebug = 0; // Enable DEBUG messages // Init Message, might remove later on("ready", function(){ sendChat("DeckMaker", "/w GM DeckMaker is ready !"); }); // Listen to the chat and respond to commands on("chat:message", function(msg){ // Listen only to API calls with the correct prefix if(msg.type == "api" &amp;&amp; msg.content.indexOf("!Deckmaker ") !== -1) { // Stores the message without the api prefix var command = msg.content.replace("!Deckmaker ", ""); var commandWords = command.split(' '); // example : !Deckmaker addCards DeckName 2 Ghost 2 Mario 1 Sirion 3 Prion if (commandWords[0] == "addCards"){ var toDeckName = commandWords[1]; var index; sendChat("DeckMaker", "/w GM Deckmaker will now try to add cards"); for (index = 2; index &lt; commandWords.length; index = index + 2){ try { AddSingleCard(commandWords[index+1], toDeckName, Number(commandWords[index])); if (IsDebug == 1){sendChat("DeckMaker", "/w GM DEBUG : " + commandWords[index+1] + " " + commandWords[index]);} } catch (error) { sendChat("DeckMaker", "/w GM Deckmaker Error At : "+commandWords[index]+" "+commandWords[index+1]); } } sendChat("DeckMaker", "/w GM Completed."); } // Very rough help if (commandWords[0] == "help"){ sendChat("DeckMaker", "/w GM !Deckmaker addCards yourdeckname numberofcard1 cardname1 numberofcard2 cardname2.... "); } } }); function AddSingleCard(cardName, toDeckName, amount = 1){ // Find the id of the first deck with the name "toDeckName" var toDeckId = findObjs({ type: 'deck', name: toDeckName })[0].id; // Find the id of the card bank. var fromDeckId = findObjs({ type: 'deck', name: cardBankName })[0].id; CopyCardToDeck(toDeckId, fromDeckId, cardName, amount); } /// Makes copies of a card with the name "cardName" within the deck with the id "fromDeckId" /// and adds an amount of them to the deck with the id "toDeckId" function CopyCardToDeck(toDeckId, fromDeckId, cardName, amount = 1){ // Attempts to find the card. var cardToCopy = findObjs({ type: 'card', deckid: fromDeckId, name: cardName })[0]; if (IsDebug == 1){sendChat("DeckMaker", "/w GM DEBUG : " + amount + " " + cardName);} // Create copies var i; for (i = 1; i &lt; amount+1; i++) { // I think this is an okay way to copy stuff ? createObj('card',{ // Names card like "BlueEyes (1)", "BlueEyes (2)" etc. // Change naming convention here name: cardToCopy.get('name') + "(" + i + ")", deckid: toDeckId, avatar: cardToCopy.get('avatar') }); } // Returns 1 if the function didn't blow up. return 1; }
1600261312
The Aaron
Roll20 Production Team
API Scripter
Nice first script!&nbsp; I do have some feedback, if you're interested: 1) You should wrap the whole thing in your on('ready',...) event, or a javascript closure.&nbsp; Javascript lacks declared namespaces an all the API scripts share the global namespace, so if another script has a variable named IsDebug, there might be problems.&nbsp; Best to keep the global namespace as clean as possible. 2) Prefer let or const over var.&nbsp; There's probably a long discussion of this in those threads I linked, but suffice it to say that let and const behave sanely, var has some weirdness and should just be avoided.&nbsp; You can pretty much find/replace all cases of var with let and it should be fine. 3) Using String.startsWith() or a regular expression like /^!deckmaker/i.test() is preferable to checking .indexOf() !== -1.&nbsp; The latter will match in the middle of a string, which will cause problems if someone is passing your command as an argument to another API script.&nbsp; Both the former options will check against the beginning of the string, and the regular expression is ignoring case, which is nice. 4) Splitting a string on /\s+/ is nicer than ' ', as the regular expression will consume all spaces between, so an errant extra space won't make parsing strange. 5) If you want to handle names with spaces in them, an easy way to do that is to adopt a syntax like: !COMMAND --ARGUMENT --ARGUMENT WITH SPACES --OTHER ARGUMENT Then just split on /\+--/ and you have each argument phrase, which can be individually parsed into parts.&nbsp; it's pretty common on the API to use | (pipe) as a separator for sub arguments to make it easier: !COMMAND --ARGUMENT --ARGUMENT WITH SPACES|AND EXTRAS --OTHER ARGUMENT|CAN|HAVE|MANY 6) You should always use the longer comparisons of === and !== instead of == and !=, until you understand the difference between them, at which point you'll never want to use the short ones. =D All in all, a pretty neat script, I'm looking forward to trying it out!
After checking TheAaron's feedback, here is a revised version of my script, it can still be improved but it's now way more accepting when it comes to entering arguments. It also allows deck and card names with spaces in them. // Main Wrapper on("ready", function(){ let cardBankName = "CardBank"; // Change this to the name of the card bank let IsDebug = 0; // Enable DEBUG messages // Listen to the chat and respond to commands on("chat:message", function(msg){ // Listen only to API calls with the correct prefix if(msg.type == "api" &amp;&amp; /^!deckmaker/i.test(msg.content)) { // Stores the message without the api prefix let command = msg.content.replace(/!deckmaker/i, ""); let commandWords = command.split(/--/); // Removes '--' from arguments and filter out whitespace only arguments commandWords.forEach(element =&gt; element.split("--").pop()) commandWords = commandWords.filter(function(entry) { return /\S/.test(entry); }); if (IsDebug===1){sendChat("DEBUG", "/w GM Command words : "+commandWords.length);} // example : !Deckmaker addCards DeckName 2 Ghost 2 Mario 1 Sirion 3 Prion if (/addcards/i.test(commandWords[0])){ let toDeckName = commandWords[1].trim(); // Checks if the deck exists if (findObjs({type: 'deck',name: toDeckName})[0] !== undefined) { sendChat("DeckMaker", "/w GM Deckmaker will now try to add cards"); // Split arguments on pipe character let cardToCopyList = commandWords[2].split("|"); let index; for (index = 0; index &lt; cardToCopyList.length; index++){ // Gets the amount and cardname separated by cutting the string in two pieces. Trim in order to remove any extra spaces let amount = cardToCopyList[index].trim().split(/\s(.+)/)[0].trim(); let cardName = cardToCopyList[index].trim().split(/\s(.+)/)[1].trim(); // Try to add the card try { AddSingleCard(cardName, toDeckName, Number(amount)); } catch(error) { sendChat("DeckMaker", "/w GM Couldn't find the card '"+cardName+"' in the card bank."); } } sendChat("DeckMaker", "/w GM Completed."); } else { sendChat("DeckMaker", "/w GM No deck with the name '"+toDeckName+"' was found."); } } // Very rough help if (/help/i.test(commandWords[0])){ sendChat("DeckMaker", "/w GM Syntax : !Deckmaker --AddCards --DeckName --Number1 CardName1|Number2 CardName2|Number3... "); } } }); function AddSingleCard(cardName, toDeckName, amount = 1){ // Find the id of the first deck with the name "toDeckName" let toDeckId = findObjs({ type: 'deck', name: toDeckName })[0].id; // Find the id of the card bank. let fromDeckId = findObjs({ type: 'deck', name: cardBankName })[0].id; CopyCardToDeck(toDeckId, fromDeckId, cardName, amount); } /// Makes copies of a card with the name "cardName" within the deck with the id "fromDeckId" /// and adds an amount of them to the deck with the id "toDeckId" function CopyCardToDeck(toDeckId, fromDeckId, cardName, amount = 1){ // Attempts to find the card. let cardToCopy = findObjs({ type: 'card', deckid: fromDeckId, name: cardName })[0]; if (IsDebug == 1){sendChat("DeckMaker", "/w GM DEBUG : " + amount + " " + cardName);} // Create copies let i; for (i = 1; i &lt; amount+1; i++) { // I think this is an okay way to copy stuff ? createObj('card',{ // Names card like "BlueEyes (1)", "BlueEyes (2)" etc. // Change naming convention here name: cardToCopy.get('name') + "(" + i + ")", deckid: toDeckId, avatar: cardToCopy.get('avatar') }); } // Returns 1 if the function didn't blow up. return 1; } sendChat("DeckMaker", "/w GM DeckMaker is ready !"); });
1600373039
The Aaron
Roll20 Production Team
API Scripter
Nice! No need to do anything, but in case you're interested, here's a few more thoughts. You can probably replace: let command = msg.content.replace(/!deckmaker/i, ""); let commandWords = command.split(/--/); // Removes '--' from arguments and filter out whitespace only arguments commandWords.forEach(element =&gt; element.split("--").pop()) commandWords = commandWords.filter(function(entry) { return /\S/.test(entry); }); with: let commandWords = msg.content.split(/\s+--/).slice(1); This will split the text on 1 or more spaces followed by two dashes, and remove both, then return an array without the first element (from the element at position 1 forward): "!foo --bar baz --qux --fib".split(/\s+--/).slice(1); (3)&nbsp;["bar baz", "qux", "fib"] Similarly, this will assure no extra spaces on the pipe splits: let cardToCopyList = commandWords[2].split(/\s*\|\s*); example: "foo|bar |baz | qux".split(/\s*\|\s*/); (4)&nbsp;["foo", "bar", "baz", "qux"] For commands that take a sub command, I often find the switch statement to be more natural: switch(commands.shift().toLowerCase()){ case 'addcards': { // stuff } break; case 'help': { // stuff } break; } If you are going to match single words with a regular expression, you might want to add start and end markers: /^addcards$/i.test(commands[0])