Advertisement Create a free account

Any way to change the image on an existing NPC token?

1528818871
The specific use case is that I've purchased the Tomb of Annihilation module.  The included tokens are fine, but I have a collection of token images from prior VTT's I've used that I prefer.  I'd like to be able to swap out the images without having to re-create every npc token on the maps.  I'd love to be able, for example swap out the "Yuan-Ti Malison" image used throughout the adventure with a new one.  If they had to be done one-by-one, I might be willing to do that too.  I'd upgrade to a pro subscription to do this, if the tools exist there.   Possible?
1528819524
keithcurtis
Roll20 Mod Team
It could be done on a one-by-one basis with the token-mod script.I don't think there is a way to globally change all tokens across the game that have already been placed. You would have to change them individually, or change one, save it as default and drag it in to replace tokens one-by-one. I'm pretty sure you could set up a macro that would allow you to drag an image to the map, and use @{selected} and @{target} to switch them out and update to new default token in one step.
1528819805
The Aaron
Pro
API Scripter
That could be done with the API (a Pro subscriber Perk).  With Plus, the best you could do would be to configure the default token to the image you want, and drag out replacements on each map.
1528840082
Kastion
Pro
API Scripter
The Aaron said: That could be done with the API (a Pro subscriber Perk).  With Plus, the best you could do would be to configure the default token to the image you want, and drag out replacements on each map. Building on what The Aaron has said if you go Pro you can install a script called TokenMod [Shameless plug for the scriptomancer =P]. This script lets you change pretty much anything about a token including the "imgsrc" of a token to change that token to a new image. Example: !token-mod --set imgsrc|https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580 This will set the SELECTED token to the new imgsrc. From the Docs: "Setting the token image to a library image using a URL."
1528840845
The Aaron
Pro
API Scripter
That would definitely work.  I was thinking more along the lines of a script that would: Find all tokens that represent a character If the token's imgsrc doesn't match the defaulttoken's imgsrc and the defaulttoken's imgsrc Change the token's imgsrc to match the defaulttoken. That would have the advantage of being a one-and-done script you run when you change a defaulttoken and want to updated every instance on every map page.  =D
1528842337
The Aaron
Pro
API Scripter
Something like this (untested) code snippet... on('ready', () => {     const getCleanImgsrc = (imgsrc) => {        let 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;     };     on('chat:message', (msg) => {         if('api' !== msg.type || !playerIsGM(msg.playerid) || !/^!update-tokens-to-default/i.test(msg.content)){             return;         }         let characters = findObjs({type:'character'})             .filter((c)=> /(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/.test(JSON.parse(c.get('defaulttoken')).imgsrc));         let handleCharacter = ()=>{             let c = characters.shift();             if(c){                 const dt = JSON.parse(c.get('defaulttoken'));                 const dtimgsrc = getCleanImgsrc(dt.imgsrc);                 findObjs({                     type: 'graphic',                     represents: c.id                 }).forEach((t)=>{                     if(dtimgsrc && getCleanImgsrc(t.get('imgsrc')) !== dtimgsrc){                         t.set({imgsrc: dtimgsrc});                     }                 });             }             if(characters.length){                 _.defer(handleCharacter);             }         };         handleCharacter();     }); });
1528842448
keithcurtis
Roll20 Mod Team
Could that be folded into Token Sync? (Isn't that one of yours, Aaron?)
1528851534
The Aaron
Pro
API Scripter
That one's not mine.  Not sure whose it is, but I have a copy in my "_Others" folder, so it came from sone else. =D Probably could be rolled in.
1528920813
Thanks, this has been super-helpful.  I've upgraded and started playing with the API (not sure if it makes sense to move this thread) I'm working through your code above to try and understand what its doing.  Can I ask what this line is supposed to do? let characters = findObjs({type:'character'}) .filter((c)=> /(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/.test(JSON.parse(c.get('defaulttoken')).imgsrc)); When I try to use that line out of context (I'm just trying to get a handle on the objects and what their attributes are) I get this error: Error: You must pass a callback function to .get() when getting the bio, notes, defaulttoken, or gmnotes of a Character or Handout.
1528924330

Edited 1529073457
The Aaron
Pro
API Scripter
DANG IT!.  I just spent the last 45 minutes typing a 3 page response to this with many many examples and details... and the browser lost it. =( So, I'm going to summarize and say "Oh, that won't work, try this:" on('ready', () => {     const getCleanImgsrc = (imgsrc) => {        let 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;     };     on('chat:message', (msg) => {         if('api' !== msg.type || !playerIsGM(msg.playerid) || !/^!update-tokens-to-default/i.test(msg.content)){             return;         }         let characters = findObjs({type:'character'});         let handleCharacter = ()=>{             let c = characters.shift();             if(c){                 c.get('defaulttoken',(deftok) => {                     const dt = JSON.parse(deftok);                     const dtimgsrc = getCleanImgsrc((dt||{imgsrc:’’}).imgsrc);                     if(dtimgsrc){                         findObjs({                             type: 'graphic',                             represents: c.id                         }).forEach((t)=>{                             if(getCleanImgsrc(t.get('imgsrc')) !== dtimgsrc){                                 t.set({imgsrc: dtimgsrc});                             }                         });                     }                 });             }             if(characters.length){                 _.defer(handleCharacter);             }         };         handleCharacter();              }); }); Slightly more details: I forgot that defaulttoken was one of the properties you have to fetch asynchronously, so it won't work to filter with it.  The intent of that line was go get a list of all the characters, then trim it down to only the ones with a default token with an imgsrc in a user library. How well do you know Javascript?  Since I'm super frustrated about my great description getting sacrificed on the alter of the internet, rather than recreate it I'll just ask that and tailor to what you know. =D    If the answer is "I don't really know Javascript that well" then I'll come back later after a few shots of something and post a better discussion then! =D
1528938539
>>How well do you know Javascript? I'm an old C/C++ programmer, so I can handle the programming concepts.  I've done only a bit of javascript, so some of the language constructs trip me up until I google or stack overflow them, and of course I'm new to Roll20 so some of the details aren't clear yet (like, why do you have to "clean" the imagesrc?).  I'll play around with your new code and let you know if I get tripped up again.  Thanks again!
1528940167
The Aaron
Pro
API Scripter
Sweet!  I'm an old (well, middle aged...) C/C++ programmer, too!  I aways tell people I write my code in C++ in my head and then translate it to the language I'm using. =D The API is only able to create tokens (and other images like avatars) if the image is in a user library.  Images in the Marketplace will fail.  getCleanImgsrc() is a function I wrote to tell if the url of an image is in a user library, and convert it to the format the API can use (only "thumb" image urls) and a few other house keeping details. Happy to chat about any programming stuff!
1528943793
Is it possible for characters to not have default tokens?  In looking at some characters, it seems so.  Some have avatars, but not default tokens.  Sometimes the reverse.  This line in the code above has been failing: const dtimgsrc = getCleanImgsrc(dt.imgsrc) TypeError: Cannot read property 'imgsrc' of null
1528944472
The Aaron
Pro
API Scripter
Ah, yes they could. This should fix it: const dtimgsrc = getCleanImgsrc((dt||{imgsrc:’’}).imgsrc);
1529072355
I just wanted to let you know that your code above seems to work!  I modified it a bit, as I've decided to do a more targeted approach (select a token, then all tokens representing that character are updated to the default).  Thanks for your help!
1529073416
The Aaron
Pro
API Scripter
No problem!  Glad to hear you got it working!