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

API Script for Token Generation/Editing

1700475531

Edited 1700480752
Hey! I am working on a way to generate generic humanoid monsters with randomized traits as quickly as possible on the fly. I currently use the MonsterHitDice API to generate HP values when I drag out a character sheet, which is great, except I have to make separate character sheet for each monster tier and their hit dice (Humanoid L1 with 1d6HD, Humanoid L2 with 2d6HD, ect.). No big deal. I am imagining a way to use an API to do the following things when I drop certain tokens onto the tabletop: 1. Fill the token Name field with a random trait from a Rollable Table (blade scar, missing finger, ect.). 2. Possibly add a random token badge from a specific set. Are these ideas a pipedream? I would greatly appreciate help and advice in this matter! Gratefully, Ragnar
Ragnar, you mention that you want this to happen when you drop certain tokens. While it's possible to trigger things when graphics are added, that would trigger each time any token was dragged out, so what would you want to use to identify tokens that need to be generated versus tokens that should be left alone?
1700497028
The Aaron
Roll20 Production Team
API Scripter
Ok, here's my solution to this.&nbsp; It will match based on the image url currently, but that can be changed to say the character id or some identifier in the character or token name.&nbsp; For now, edit the script and add as many images as you want to the matchImages array.&nbsp; I suggest one per line, be sure to put " " around them, and put a , at the end (last one doesn't need a comma, but having one won't hurt).&nbsp; You can get the image url by selecting a token, hitting Z, and then right click, Copy Image Address. It will look for a table named "trait-table" and use that as the random name of the token.&nbsp; It will look for a table named "status-table" and pull a random status marker name from that table.&nbsp; Be sure to use the full name for custom tokens (like "Moon:12341234").&nbsp; You can get a list of all available status marker names easily from TokenMod with: !token-mod --help-statusmarkers once you've set up those 3 things, you can drag in tokens (or copy paste them) with those image urls and it will rename and apply status markers to them.&nbsp; It will also turn on show name and showplayers_name so that players can see the token name. Code: on('ready',()=&gt;{ const traitTableName = 'trait-table'; const statusTableName = 'status-table'; const matchImages = [ "<a href="https://s3.amazonaws.com/files.d20.io/some/image/url/here/med.png?12341234123" rel="nofollow">https://s3.amazonaws.com/files.d20.io/some/image/url/here/med.png?12341234123</a>", "<a href="https://s3.amazonaws.com/files.d20.io/some/other/image/url/here/med.png?12341234123" rel="nofollow">https://s3.amazonaws.com/files.d20.io/some/other/image/url/here/med.png?12341234123</a>" ]; const imgmatch = new RegExp(`http[s]?://(?:s3.amazonaws.com/)?files.d20.io/(?:images|marketplace)/([^/]*/[^/]*)/`,''); const isMatchToken = (()=&gt; { const matchCache = matchImages.map(i=&gt;i.match(imgmatch)[1]); return (obj) =&gt; matchCache.includes(obj.get('imgsrc').match(imgmatch)[1]); })(); let tokenIds = []; const processInlinerolls = (msg) =&gt; { if(msg.hasOwnProperty('inlinerolls')){ return msg.inlinerolls .reduce((m,v,k) =&gt; { let ti=v.results.rolls.reduce((m2,v2) =&gt; { if(v2.hasOwnProperty('table')){ m2.push(v2.results.reduce((m3,v3) =&gt; [...m3,(v3.tableItem||{}).name],[]).join(", ")); } return m2; },[]).join(', '); return [...m,{k:`$[[${k}]]`, v:(ti.length &amp;&amp; ti) || v.results.total || 0}]; },[]) .reduce((m,o) =&gt; m.replace(o.k,o.v), msg.content); } else { return msg.content; } }; const setTokenThings = (obj,prev,force=false) =&gt; { if(tokenIds.includes(obj.id) || force){ tokenIds=tokenIds.filter(id =&gt; id !== obj.id); if( 'graphic' === obj.get('type') &amp;&amp; 'token' === obj.get('subtype') &amp;&amp; isMatchToken(obj) ){ sendChat('',`[[1t[${traitTableName}]]]%%SEP%%[[1t[${statusTableName}]]]`,r=&gt;{ let n = processInlinerolls(r[0]).split(/%%SEP%%/); obj.set({ showname:true, showplayers_name:true, name: n[0], statusmarkers: n[1] }); }); } } }; const saveTokenId = (obj) =&gt; { tokenIds.push(obj.id); }; on('add:graphic', saveTokenId); on('change:graphic', setTokenThings); });
1700646296

Edited 1700648844
Awesome!! I will give this a try! So TheAaron, when you say to add that code to the existing code, I assume you mean to the MonsterHitDice script? Or do I create a new script? Also, is it possible to have this script look for different token status icons based on the image I drag out? For example, say I have a "humanoid fighter" token that I want to have it roll from a "martial weapons" status table. Then a "humanoid rogue" token I want to roll from a "humanoid rogue" table? Is this type of flexibility/granularity possible within one script, or could I make separate scripts for each? (bad idea? lol) Thank you SO much for your help! I really appreciate it! Ragnar
Update: It's working like a charm!! I'm stoked!&nbsp; Thank you Aaron for your help!
1700693612
The Aaron
Roll20 Production Team
API Scripter
That's definitely something that a script is capable of.&nbsp; The easiest for you is probably to just make copies as you surmised, and only include images and table names for the tokens in question. Long term, it's probably better to adjust the script to not rely on the image url at all, that was just something easy to use based on your original ask.&nbsp; Better might be to have some structure in the GM Notes of a token or character telling what tables to use to adjust the name and status markers.&nbsp; The script would then inspect the token or character it represents and use the tables, or do nothing if there was no annotation.
Oh dang! That sounds amazing! That is the exact kind of mechanic that would make this kind of thing completely customizable. I'm sure you're busy, but I would LOVE to learn to edit the script to make it work like that. Can you show me the ropes when you get time?? Thanks again SO MUCH for your help on this! I really appreciate you taking the time to do all this for me!
1701170078

Edited 1701170330
OK, so if I was going to replace the portion of the script with a code that will read a character sheet's GM notes, what would what look like? I have done some searching to see if I can find out how from a different post, but I am not finding what I am looking for. This is the script that has been working great based on our conversation so far. It creates a token with a randomized token status icons for amateur&nbsp;fighters with simple weapons, and changes the token name to a random trait that distinguishes the combatant from the rest. on('ready',()=&gt;{ const traitTableName = 'trait-table'; const statusTableName = 'status-table-amateur'; const matchImages = [ "<a href="https://files.d20.io/images/313141675/3hsAEWF7yBTgmsKTDvcYHg/original.png?16678156445" rel="nofollow">https://files.d20.io/images/313141675/3hsAEWF7yBTgmsKTDvcYHg/original.png?16678156445</a>", ]; const imgmatch = new RegExp(`http[s]?://(?:s3.amazonaws.com/)?files.d20.io/(?:images|marketplace)/([^/]*/[^/]*)/`,''); const isMatchToken = (()=&gt; { const matchCache = matchImages.map(i=&gt;i.match(imgmatch)[1]); return (obj) =&gt; matchCache.includes(obj.get('imgsrc').match(imgmatch)[1]); })(); let tokenIds = []; const processInlinerolls = (msg) =&gt; { if(msg.hasOwnProperty('inlinerolls')){ return msg.inlinerolls .reduce((m,v,k) =&gt; { let ti=v.results.rolls.reduce((m2,v2) =&gt; { if(v2.hasOwnProperty('table')){ m2.push(v2.results.reduce((m3,v3) =&gt; [...m3,(v3.tableItem||{}).name],[]).join(", ")); } return m2; },[]).join(', '); return [...m,{k:`$[[${k}]]`, v:(ti.length &amp;&amp; ti) || v.results.total || 0}]; },[]) .reduce((m,o) =&gt; m.replace(o.k,o.v), msg.content); } else { return msg.content; } }; const setTokenThings = (obj,prev,force=false) =&gt; { if(tokenIds.includes(obj.id) || force){ tokenIds=tokenIds.filter(id =&gt; id !== obj.id); if( 'graphic' === obj.get('type') &amp;&amp; 'token' === obj.get('subtype') &amp;&amp; isMatchToken(obj) ){ sendChat('',`[[1t[${traitTableName}]]]%%SEP%%[[1t[${statusTableName}]]]`,r=&gt;{ let n = processInlinerolls(r[0]).split(/%%SEP%%/); obj.set({ showname:true, showplayers_name:true, name: n[0], statusmarkers: n[1] }); }); } } }; const saveTokenId = (obj) =&gt; { tokenIds.push(obj.id); }; on('add:graphic', saveTokenId); on('change:graphic', setTokenThings); }); It's fantastic, but it would be great to be able to turn the randomized combatant into an NPC with it's own character sheet without having to change the token's image. If it referenced the generic character sheet I dragged out to create it, I can create a character sheet for the NPC and just assign the token to it, and the API won't randomly scramble anything when I drag out the newly created NPC (plus, in general, it makes the script much more flexible). So I guess my question is, how do I change the API to reference character GM notes instead of token image? Thanks again for the help!! Ragnar
1701186772

Edited 1701188594
The Aaron
Roll20 Production Team
API Scripter
Sorry for the delay, holidays and such. =D So, doing it from text in the GM Notes is a bit complicated.&nbsp; I'd set it up so you have lines like this: trait: trait-table status: status-table Then look for those lines on newly created tokens.&nbsp; Here are the complicated parts: First, the notes are stored in an escaped form: %3Cp%3Etraits%3A%20trait-table%3C/p%3E%3Cp%3Estatus%3A%20status-table%3C/p%3E You can convert that back to readable with the unescape function, which will give you: &lt;p&gt;traits: trait-table&lt;/p&gt;&lt;p&gt;status: status-table&lt;/p&gt; Next you have to split this into reasonable lines.&nbsp; HTML has elements that are "block elements", meaning they define a single unit.&nbsp; I threw together a function that will find the start and end of the blocks and split on them, giving an array of conceptual lines: [ "&lt;p&gt;traits: trait-table&lt;/p&gt;", "&lt;p&gt;status: status-table&lt;/p&gt;" ] Then strip out all the HTML tags: [ "traits: trait-table", "status: status-table" ] Next write a function that pulls out the "key: value" pairs for "traits" and "status", and you have the settings. The next complication is that you don't want to pass bad table names to the roll function or it will crash the API.&nbsp; For that, I wrote a function that verifies that a table for that name exists and returns the inline roll for it.&nbsp; The function I wrote is a little bit slow (because it does a find for every execution, I'll probably tidy that up with some lazy caching later.&nbsp;&nbsp; Edit: Ok, it's later... I went ahead and added caching for the existence of rollable tables.&nbsp; There were a few complications with that, which made it interesting. =D&nbsp; Table names are not unique, so it needs to track the table id as well.&nbsp; Additionally, adding new tables, they always are named "new-table", so it needs to handle changing the names of tables to something reasonable.&nbsp; Fun, fun! Anyway, here is the code with those changes: on('ready',()=&gt;{ const getNoteLines = (()=&gt; { const blockElements = [ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'pre', 'address', 'blockquote', 'dl', 'div', 'fieldset', 'form', 'hr', 'noscript', 'table','br' ]; const rStart=new RegExp(`&lt;\\s*(?:${blockElements.join('|')})\\b[^&gt;]*&gt;`,'ig'); const rEnd=new RegExp(`&lt;\\s*\\/\\s*(?:${blockElements.join('|')})\\b[^&gt;]*&gt;`,'ig'); return (str) =&gt; (rStart.test(str) ? str .replace(/[\n\r]+/g,' ') .replace(rStart,"\r$&amp;") .replace(rEnd,"$&amp;\r") .split(/[\n\r]+/) : str .split(/(?:[\n\r]+|&lt;br\/?&gt;)/) ) .map((s)=&gt;s.trim()) .filter((s)=&gt;s.length) ; })(); const stripHTML = (t) =&gt; t.replace(/&lt;[^&gt;]*&gt;/g,''); const getSetting = (()=&gt;{ const properties = [ 'traits','status' ]; const rSetting = new RegExp(`^\\s*(${properties.join('|')})\\b\\s*:\\s*(.*)\\s*$`,'i'); return (s) =&gt; { let m = (s||'').match(rSetting); if(m){ return [m[1],m[2]]; } }; })(); const getRollString = (()=&gt;{ let tableNames = findObjs({type:'rollabletable'}).reduce((m,t)=&gt;({...m,[t.get('name')]:[...(m[t.get('name')]||[]),t.id]}),{}); on('add:rollabletable',(t)=&gt;tableNames[t.get('name')]=[...(tableNames[t.get('name')]||[]),t.id]); on('change:rollabletable',(tn,tp)=&gt;{ tableNames[tp.name]=(tableNames[tp.name]||[]).filter(i=&gt;i!==tp._id); if(0 === tableNames[tp.name].length){ delete tableNames[tp.name]; } tableNames[tn.get('name')]=[...(tableNames[tn.get('name')]||[]),tn.id]; }); on('destroy:rollabletable',(t)=&gt;{ tableNames[t]=(tableNames[t]||[]).filter(i=&gt;i!==t.id); if(0 === tableNames[t].length){ delete tableNames[t]; } }); return (t) =&gt; { if(tableNames.hasOwnProperty(t)) { return `[[1t[${t}]]]`; } return ''; }; })(); const getMatchData = (t) =&gt; getNoteLines(unescape(t.get('gmnotes'))) .map(stripHTML) .reduce((m,l)=&gt;{ let s = getSetting(l); if(s) { return { ...m, [s[0]]:s[1]}; } return m; },{}); let tokenIds = []; const processInlinerolls = (msg) =&gt; { if(msg.hasOwnProperty('inlinerolls')){ return msg.inlinerolls .reduce((m,v,k) =&gt; { let ti=v.results.rolls.reduce((m2,v2) =&gt; { if(v2.hasOwnProperty('table')){ m2.push(v2.results.reduce((m3,v3) =&gt; [...m3,(v3.tableItem||{}).name],[]).join(", ")); } return m2; },[]).join(', '); return [...m,{k:`$[[${k}]]`, v:(ti.length &amp;&amp; ti) || v.results.total || 0}]; },[]) .reduce((m,o) =&gt; m.replace(o.k,o.v), msg.content); } else { return msg.content; } }; const setTokenThings = (obj,prev,force=false) =&gt; { if(tokenIds.includes(obj.id) || force){ tokenIds=tokenIds.filter(id =&gt; id !== obj.id); if( 'graphic' === obj.get('type') &amp;&amp; 'token' === obj.get('subtype') ) { let md = getMatchData(obj); if(md.hasOwnProperty('traits')) { sendChat('',`${getRollString(md.traits)}%%SEP%%${getRollString(md.status)}`,r=&gt;{ let n = processInlinerolls(r[0]).split(/%%SEP%%/); obj.set({ showname:true, showplayers_name:true, name: n[0], statusmarkers: n[1] }); }); } } } }; const saveTokenId = (obj) =&gt; { tokenIds.push(obj.id); }; on('add:graphic', saveTokenId); on('change:graphic', setTokenThings); });
OMG Aaron... you're a beast! I am quickly realizing how wildly out of my league I am with all this XD I ignorantly told myself "how hard could it be, I just need some pointers to get started" and I have found the answer, "WAY TOO HARD FOR YOU!" You did not have to go through all this work for little ol' me... but I am extremely grateful you did! I will mess around with this and I'll be back if I run into a snag (quite likely). Gratefully, Ragnar
1701250695

Edited 1701254688
Ok so first off, is this what I write in the GM notes of the character sheets I am wanting to include? If not, what part was I supposed to add to the GM notes? [ "traits: trait-table", "status: status-table" ]
Secondly, am I able to add multiple additional tables that carry different weapon sets? Then could I tell the API which tables to roll on in the GM notes?
1701275508
The Aaron
Roll20 Production Team
API Scripter
The way it's set up above is to put it in the GM Notes on the Token, not the Character.&nbsp; I considered having it look at the Character GM Notes, but that's slightly more complicated for what seemed like not any greater benefit.&nbsp; Putting it on the Token makes it a little easier because you don't have to worry about whether the Token represents a Character. So, in the GM Notes on the Token, you just need: trait: trait-table status: status-table Just bare KEY: VALUE, not JSON encoded. You can mix and match whatever table names you want in there, so some Tokens might use "trait-swords" and another might be "trait-spells".&nbsp; Having "trait" is required to determine that the token is participating.&nbsp; If you leave "status" off, it will just not set a status marker (though there may be a bug where it will clear an existing status if "status" is missing... hmm).
Oh sick!! Even simpler and more flexible than I thought! I'll test it out now!
Ok so for some reason it doesn't seem to be working D= I have a token created the way I want, with the GM notes sporting the following: trait: trait-table status: status-table-amateur Each of these are correctly match the spelling of the rollable tables they reference. &nbsp; I tried having the token represent a character and also tried it representing no character. I have the status table referencing the exact codes for the status markers like we did before. I copy-pasted your script a second time to make sure I didn't mess something up, but for some reason nothing happens when i drop the token onto the battlefield... what am I missing? Thanks for your help once again! XD Ragnar
1701804579
The Aaron
Roll20 Production Team
API Scripter
Hmm.&nbsp; Want to PM an invite and GM me, and I'll take a look?
Sure thing