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

Token Side change Macro/API?

October 26 (7 years ago)
Nox
Translator
Hi all, i'd like to have a simple macro or script to swap the face of a token. I set up a rollable table with two image (one blue-ish and one Red-ish, that represent the Idle and the Combat stance of a golem of one of my players). I'd like to give the player the chance to change his golem's token from a macro or a script without having to right click it and manually select. Any way I can accomplish this?
October 26 (7 years ago)
The Aaron
Pro
API Scripter
Change Token Image does that. 

Here's how you get the help screen:
!change-token-img --help
Here's the version I've tweaked:
// By:       Christopher Buchholz
//           with heavy copying from, I mean use of as a Pattern, TokenMod
var ChangeTokenImg = ChangeTokenImg || (function() {
    'use strict';
    var version = '0.1.32',
    lastUpdate = 1499262126,

    observers = {
        tokenChange: []
    },

    showHelp = function(id) {
        var who=getObj('player',id).get('_displayname').split(' ')[0];
        sendChat('',
            '/w '+who+' <div style="border: 1px solid black; background-color: white; padding: 3px 3px">'
            + '<div style="padding-bottom:5px">'
            + '<span style="font-size:130%;border-bottom:1px;font-weight:bold">ChangeTokenImg v'+version+ '</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'Changes the image for selected token IF the tokens are from a Rollable Table.'
            + '<span style="color:#FF0000;">Currently works <b>ONLY</b> if images are in your personal library!</span>'
            + '</div><div>'
            + '<span style="border-bottom:1px;font-weight:bold">Usage:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'select token(s) to change, then type '
            + '<br/><b>!change-token-img</b> '
            + '<br/>with one of the following options.' 
            + '</div><div>'
            + '<span style="border-bottom:1px;font-weight:bold">Type of change:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + '<b>--flip</b> Flips image between 0 and 1 '
            + '<br/><b>--set</b> Set the image index, must be followed by a space and a number: --set 3 sets the token to the 4th image in the rollable table. '
            + '<br/><b>--incr</b> Increments the token to the next image in the rollable table '
            + '</div><div>'
            + '<br/><span style="border-bottom:1px;font-weight:bold">Modifiers:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'These can be used with any of the above options:'
            + '<br/><b>--notsame</b> Asserts scripts should NOT assume all tokens have same rollable table of images. Default is '
            + 'script assumes all selected tokens use same images.'
            + '</div><div>'
            + '<br/><span style="border-bottom:1px;font-weight:bold">Examples:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + '<b>!change-token-img --set 2 --notsame</b> change all tokens to image in index 2 (3rd image) of rollable table. For each token, '
            + 'check the rollable table for images, do NOT assume all tokens are the same.'
            + '<br/><b>!change-token-img --flip</b> for all selected tokens, check their current image displayed. If it is the first image, flip '
            + 'token to the second image. If it is set to the second image, then flip token to the first image. All tokens have the same rollable table of images.'
            + '<br/><b>!change-token-img --set 0</b> Set all tokens to the first image, assume they are all the same.'
            + '</div>'
        );
    },
    showError = function(id,n,tname,errortype) {
        var who=(getObj('player',id)||{get:()=>'API'}).get('_displayname');
        var errorstr = 'ChangeTokenImg: ';
        if (errortype == 'SIDES') {
            var sidestr='s';
            if (n == 1) {
                sidestr='';
            }
            errorstr = tname+' has only ' + n + ' side'+sidestr+'!';
        } else if (errortype == 'EMPTY') {
            errorstr = 'You must pick --flip, --set or --incr';
        } else if (errortype == 'ARG') {
            errorstr = n + ' is not a valid value for set parameter';            
        }
        sendChat('', `/w "${who}" <div>${errorstr}</div>`);
    },    
    getCleanImagesrc = function (imgsrc) {
        var parts = imgsrc.match(/(.*\/images\/.*)(thumb|max)(.*)$/);
        if(parts) {
            return parts[1]+'thumb'+parts[3];
        }
        return;
    },
    setImg = function (o,nextSide,allSides) {
        var nextURL = getCleanImagesrc(decodeURIComponent(allSides[nextSide]));
        o.set({
            currentSide: nextSide,
            imgsrc: nextURL
        });
        return nextURL;
    },
    setImgUrl = function (o, nextSide, nextURL) {
        o.set({
            currentSide: nextSide,
            imgsrc: getCleanImagesrc(nextURL)
        });		
    },
    isInt = function (value) {
        return !(value === undefined) &&  !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10)) && value >= 0;
    },
    handleInput = function(msg_orig) {
        var msg = (msg_orig),
        args, cmds,  allSides=[],  nextSide, nextURL, flipimg, setimg, incrementimg, sameimages, setval;
        flipimg = false;
        setimg = false;
        sameimages = false;
        incrementimg = false;
        nextURL='BLANK';
        if (msg.type !== "api") {
            return;
        }

        if(_.has(msg,'inlinerolls')){
            msg.content = _.chain(msg.inlinerolls)
            .reduce(function(m,v,k){
                var ti=_.reduce(v.results.rolls,function(m2,v2){
                    if(_.has(v2,'table')){
                        m2.push(_.reduce(v2.results,function(m3,v3){
                            m3.push(v3.tableItem.name);
                            return m3;
                        },[]).join(', '));
                    }
                    return m2;
                },[]).join(', ');
                m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
                return m;
            },{})
            .reduce(function(m,v,k){
                return m.replace(k,v);
            },msg.content)
            .value();
        }

        args = msg.content
        .replace(/<br\/>\n/g, ' ')
        .replace(/(\{\{(.*?)\}\})/g," $2 ")
        .split(/\s+--/);
        var isthismethod= false;
        switch(args.shift()) {
            case '!change-token-img':
                isthismethod=true;
                while(args.length) {
                    cmds=args.shift().match(/([^\s]+[|#]'[^']+'|[^\s]+[|#]"[^"]+"|[^\s]+)/g);

                    switch(cmds.shift()) {
                        case 'help':
                            showHelp(msg.playerid);
                            return;
                        case 'set':
                            if(cmds.length){
                                setimg = true;
                                setval =  cmds.shift();
                                if (! isInt(setval)) {
                                    showError(msg.playerid,setval,'','ARG');
                                    return;
                                }
                            } else {
                                showHelp(msg.playerid);
                                return;
                            }
                            break;
                        case 'flip':
                            flipimg = true;
                            break;
                        case 'incr':
                            incrementimg = true;
                            break;
                        case 'notsame':
                            sameimages = false;
                            break;
                    }
                }
        }
        if (isthismethod == false) {
            return;
        }
        if (setimg == false && flipimg == false && incrementimg == false ) {
            showError(msg.playerid,'','','EMPTY');
            return;
        } else {
            //loop through selected tokens
            _.chain(msg.selected)
            .uniq()
            .map(function(o){
                return getObj('graphic',o._id);
            })
            .reject(_.isUndefined)
            .each(function(o) {
                if (sameimages == false || allSides === undefined || allSides.length == 0  ) {
                    allSides = o.get("sides").split("|");
                }
                if ( allSides.length > 1) {
                    if (setimg == true) {
                        nextSide = setval;
                    } else {
                        nextSide = o.get("currentSide") ;
                        nextSide++;						
                        if (flipimg == true && nextSide > 1) {
                            nextSide = 0;
                        } else if (nextSide == allSides.length) {
                            nextSide = 0;
                        } 	
                    }
                    if (nextSide >= allSides.length) {
                        showError(msg.playerid,allSides.length,o.get("name"),'SIDES');
                        if (sameimages == true) {
                            //quit since they are all the same
                            return;
                        }
                    } else {
                        let prev=JSON.parse(JSON.stringify(o));
                        if (nextURL == 'BLANK' || sameimages == false) {
                            nextURL = setImg(o,nextSide,allSides);
                        } else {
                            setImgUrl(o,nextSide,nextURL);
                        }
                        notifyObservers('tokenChange',o,prev);
                    }
                } else {
                    showError(msg.playerid,allSides.length,o.get("name"),'SIDES');
                    if (sameimages == true) {
                        //quit since they are all the same
                        return;
                    }
                }
            });
        }
    },
    observeTokenChange = function(handler){
        if(handler && _.isFunction(handler)){
            observers.tokenChange.push(handler);
        }
    },
    notifyObservers = function(event,obj,prev){
        _.each(observers[event],function(handler){
            handler(obj,prev);
        });
    },
    checkInstall = function() {
        log('-=> ChangeTokenImg v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');
    },
    registerEventHandlers = function() {
        on('chat:message', handleInput);
    };
    return {
        CheckInstall: checkInstall,
        RegisterEventHandlers: registerEventHandlers,
        ObserveTokenChange: observeTokenChange
    };
}());
on("ready",function(){
    'use strict';
    ChangeTokenImg.CheckInstall();
    ChangeTokenImg.RegisterEventHandlers();
});

October 27 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
What are the major differences between your version and the One-Click?
Judging by the answer I assume it is not possible using a macro? Would like to track time by using a token and macros.
October 27 (7 years ago)
The Aaron
Pro
API Scripter

keithcurtis said:

What are the major differences between your version and the One-Click?

Hmm.. good question.. looks like nothing?  Ah... I pushed up my changes to the repo: https://github.com/Roll20/roll20-api-scripts/commi...

I don't usually do that for other people's API scripts, but in this case it was causing crashes.
October 27 (7 years ago)
The Aaron
Pro
API Scripter

Mattias H. said:

Judging by the answer I assume it is not possible using a macro? Would like to track time by using a token and macros.

Correct, the only way to automate it is via the API.
October 28 (7 years ago)

Edited October 28 (7 years ago)
Nox
Translator
The Aaron said:

Correct, the only way to automate it is via the API.


Thanks a lot Aron!! As always you solve all my problems!
Edit:
Err... I got this, and the script doesn't flip the image :( what did I do wrong? i'm using also token mod and token number.

TypeError: Cannot read property 'substring' of undefined TypeError: Cannot read property 'substring' of undefined at TrackedObj._validateAttrs (/home/node/d20-api-server/api.js:787:21) at TrackedObj.set (/home/node/d20-api-server/api.js:858:18) at setImg (apiscript.js:4716:11) at apiscript.js:4843:39 at Function._.each._.forEach (/home/node/d20-api-server/node_modules/underscore/underscore.js:153:9) at _.(anonymous function) [as each] (/home/node/d20-api-server/node_modules/underscore/underscore.js:1496:34) at handleInput (apiscript.js:4818:14) at eval (eval at (/home/node/d20-api-server/api.js:146:1), :65:16) at Object.publish (eval at (/home/node/d20-api-server/api.js:146:1), :70:8) at /home/node/d20-api-server/api.js:1510:12
October 28 (7 years ago)

Edited October 28 (7 years ago)
The Aaron
Pro
API Scripter
Hmm... hard to say from that error message. If you want to PM me an invite, I’ll come look. 
October 28 (7 years ago)
The Aaron
Pro
API Scripter
Hmm... I'm going to guess that some of your Rollable Table Token images are not in a User Library, but are in the Marketplace.

Try this version, it should tell you that's the case instead of crashing:
// By:       Christopher Buchholz
//           with heavy copying from, I mean use of as a Pattern, TokenMod
var ChangeTokenImg = ChangeTokenImg || (function() {
    'use strict';
    var version = '0.1.32',
    lastUpdate = 1509192678,


    observers = {
        tokenChange: []
    },


    showHelp = function(id) {
        var who=getObj('player',id).get('_displayname').split(' ')[0];
        sendChat('',
            '/w '+who+' <div style="border: 1px solid black; background-color: white; padding: 3px 3px">'
            + '<div style="padding-bottom:5px">'
            + '<span style="font-size:130%;border-bottom:1px;font-weight:bold">ChangeTokenImg v'+version+ '</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'Changes the image for selected token IF the tokens are from a Rollable Table.'
            + '<span style="color:#FF0000;">Currently works <b>ONLY</b> if images are in your personal library!</span>'
            + '</div><div>'
            + '<span style="border-bottom:1px;font-weight:bold">Usage:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'select token(s) to change, then type '
            + '<br/><b>!change-token-img</b> '
            + '<br/>with one of the following options.' 
            + '</div><div>'
            + '<span style="border-bottom:1px;font-weight:bold">Type of change:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + '<b>--flip</b> Flips image between 0 and 1 '
            + '<br/><b>--set</b> Set the image index, must be followed by a space and a number: --set 3 sets the token to the 4th image in the rollable table. '
            + '<br/><b>--incr</b> Increments the token to the next image in the rollable table '
            + '</div><div>'
            + '<br/><span style="border-bottom:1px;font-weight:bold">Modifiers:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + 'These can be used with any of the above options:'
            + '<br/><b>--notsame</b> Asserts scripts should NOT assume all tokens have same rollable table of images. Default is '
            + 'script assumes all selected tokens use same images.'
            + '</div><div>'
            + '<br/><span style="border-bottom:1px;font-weight:bold">Examples:</span>'
            + '</div><div style="padding-left:10px;padding-right:10px;padding-bottom:5px">'
            + '<b>!change-token-img --set 2 --notsame</b> change all tokens to image in index 2 (3rd image) of rollable table. For each token, '
            + 'check the rollable table for images, do NOT assume all tokens are the same.'
            + '<br/><b>!change-token-img --flip</b> for all selected tokens, check their current image displayed. If it is the first image, flip '
            + 'token to the second image. If it is set to the second image, then flip token to the first image. All tokens have the same rollable table of images.'
            + '<br/><b>!change-token-img --set 0</b> Set all tokens to the first image, assume they are all the same.'
            + '</div>'
        );
    },
    showError = function(id,n,tname,errortype) {
        var who=(getObj('player',id)||{get:()=>'API'}).get('_displayname');
        var errorstr = 'ChangeTokenImg: ';
        if (errortype == 'SIDES') {
            var sidestr='s';
            if (n == 1) {
                sidestr='';
            }
            errorstr = tname+' has only ' + n + ' side'+sidestr+'!';
        } else if (errortype == 'EMPTY') {
            errorstr = 'You must pick --flip, --set or --incr';
        } else if (errortype == 'ARG') {
            errorstr = n + ' is not a valid value for set parameter';            
        }
        sendChat('', `/w "${who}" <div>${errorstr}</div>`);
    },    
    getCleanImagesrc = function (imgsrc) {
        var parts = imgsrc.match(/(.*\/images\/.*)(thumb|max)(.*)$/);
        if(parts) {
            return parts[1]+'thumb'+parts[3];
        }
        return;
    },
	report = function (o,img){
		sendChat('ChangeTokenImage',`/w gm <div><img src="${img}" style="max-width: 3em;max-height: 3em;border:1px solid #333; background-color: #999; border-radius: .2em;">Image not in User Library on token ${o.get('name')}</div>`);
	},
    setImg = function (o,nextSide,allSides) {
        var nextURL = getCleanImagesrc(decodeURIComponent(allSides[nextSide]));
		if(nextURL) {
			o.set({
				currentSide: nextSide,
				imgsrc: nextURL
			});
		} else {
			report(o,allSides[nextSide]);
		}
        return nextURL;
    },
    setImgUrl = function (o, nextSide, nextURL) {
        var imgURL = getCleanImagesrc(nextURL);
		if(imgURL){
			o.set({
				currentSide: nextSide,
				imgsrc: imgURL
			});		
		} else {
			report(o,nextURL);
		}
    },
    isInt = function (value) {
        return !(value === undefined) &&  !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10)) && value >= 0;
    },
    handleInput = function(msg_orig) {
        var msg = (msg_orig),
        args, cmds,  allSides=[],  nextSide, nextURL, flipimg, setimg, incrementimg, sameimages, setval;
        flipimg = false;
        setimg = false;
        sameimages = false;
        incrementimg = false;
        nextURL='BLANK';
        if (msg.type !== "api") {
            return;
        }


        if(_.has(msg,'inlinerolls')){
            msg.content = _.chain(msg.inlinerolls)
            .reduce(function(m,v,k){
                var ti=_.reduce(v.results.rolls,function(m2,v2){
                    if(_.has(v2,'table')){
                        m2.push(_.reduce(v2.results,function(m3,v3){
                            m3.push(v3.tableItem.name);
                            return m3;
                        },[]).join(', '));
                    }
                    return m2;
                },[]).join(', ');
                m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
                return m;
            },{})
            .reduce(function(m,v,k){
                return m.replace(k,v);
            },msg.content)
            .value();
        }


        args = msg.content
        .replace(/<br\/>\n/g, ' ')
        .replace(/(\{\{(.*?)\}\})/g," $2 ")
        .split(/\s+--/);
        var isthismethod= false;
        switch(args.shift()) {
            case '!change-token-img':
                isthismethod=true;
                while(args.length) {
                    cmds=args.shift().match(/([^\s]+[|#]'[^']+'|[^\s]+[|#]"[^"]+"|[^\s]+)/g);


                    switch(cmds.shift()) {
                        case 'help':
                            showHelp(msg.playerid);
                            return;
                        case 'set':
                            if(cmds.length){
                                setimg = true;
                                setval =  cmds.shift();
                                if (! isInt(setval)) {
                                    showError(msg.playerid,setval,'','ARG');
                                    return;
                                }
                            } else {
                                showHelp(msg.playerid);
                                return;
                            }
                            break;
                        case 'flip':
                            flipimg = true;
                            break;
                        case 'incr':
                            incrementimg = true;
                            break;
                        case 'notsame':
                            sameimages = false;
                            break;
                    }
                }
        }
        if (isthismethod == false) {
            return;
        }
        if (setimg == false && flipimg == false && incrementimg == false ) {
            showError(msg.playerid,'','','EMPTY');
            return;
        } else {
            //loop through selected tokens
            _.chain(msg.selected)
            .uniq()
            .map(function(o){
                return getObj('graphic',o._id);
            })
            .reject(_.isUndefined)
            .each(function(o) {
                if (sameimages == false || allSides === undefined || allSides.length == 0  ) {
                    allSides = o.get("sides").split("|");
                }
                if ( allSides.length > 1) {
                    if (setimg == true) {
                        nextSide = setval;
                    } else {
                        nextSide = o.get("currentSide") ;
                        nextSide++;						
                        if (flipimg == true && nextSide > 1) {
                            nextSide = 0;
                        } else if (nextSide == allSides.length) {
                            nextSide = 0;
                        } 	
                    }
                    if (nextSide >= allSides.length) {
                        showError(msg.playerid,allSides.length,o.get("name"),'SIDES');
                        if (sameimages == true) {
                            //quit since they are all the same
                            return;
                        }
                    } else {
                        let prev=JSON.parse(JSON.stringify(o));
                        if (nextURL == 'BLANK' || sameimages == false) {
                            nextURL = setImg(o,nextSide,allSides);
                        } else {
                            setImgUrl(o,nextSide,nextURL);
                        }
                        notifyObservers('tokenChange',o,prev);
                    }
                } else {
                    showError(msg.playerid,allSides.length,o.get("name"),'SIDES');
                    if (sameimages == true) {
                        //quit since they are all the same
                        return;
                    }
                }
            });
        }
    },
    observeTokenChange = function(handler){
        if(handler && _.isFunction(handler)){
            observers.tokenChange.push(handler);
        }
    },
    notifyObservers = function(event,obj,prev){
        _.each(observers[event],function(handler){
            handler(obj,prev);
        });
    },
    checkInstall = function() {
        log('-=> ChangeTokenImg v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');
    },
    registerEventHandlers = function() {
        on('chat:message', handleInput);
    };
    return {
        CheckInstall: checkInstall,
        RegisterEventHandlers: registerEventHandlers,
        ObserveTokenChange: observeTokenChange
    };
}());
on("ready",function(){
    'use strict';
    ChangeTokenImg.CheckInstall();
    ChangeTokenImg.RegisterEventHandlers();
});


November 01 (7 years ago)
The Aaron
Pro
API Scripter
Ah, tracked this issue down.  I pushed a new version to the repo, but you can get it here: https://github.com/shdwjk/roll20-api-scripts/blob/...

The issue turned out to be that the images dragged in on the Rollable Table Token had the med.png version, which the script's older copy of GetCleanImagesrc was failing to properly filter.