That being said, say I'm trying to avoid doing the character sheet thing. I still like the roll table for the tokens personally, especially since I can set the 'currentside' easily. So I prefer to simply adjust my Druids token/sheet each time she changes shape through macro use. I essentially have two macros that need to fire off when my Druid Wild Shapes. 1. The !token-mod macro that changes the image, size, hp bar, and sight. 2. The !setattr macro that adjusts my druids STR, DEX, CON to the beasts and back. These work great separately but I cant put all of the information within a single macro or the information gets jumbled around. So, first question, is there a way to combine the two macros into one seamlessly. OR. Is there a clean way to fire off both macros at once with one click? If you can wait until tomorrow, I'm just about done with a solution that only involves having just one duplicate character per Druid. I just need to verify the setting of skill/save proficiency and then there's the (not so) little matter of data entry to populate the list of Wild Shapes available. It will require installing one additional API (which I had to tweak slightly to get it to work), but you can definitely string multiple macro/API calls together in a single macro which is what I'm doing to accomplish things. You just need to be sure that you don't have spaces between the lines of one API call and the next. For example, here's what I'm currently using to turn a Druid (or rather his duplicate) named Wild Shape Scenery into a Giant Rat: !token-mod {{ --set represents|"Wild Shape Scenery" name|"Giant Rat Scenery" currentside|2 scale|1u light_radius|60 light_dimradius|0 bar1_value|30 bar1_max| bar2_value| bar2_max| bar3|[[2d6]] }} !setattr {{ --charid -M0vUlsQ3rtb1LtNN8j7 --strength#7 --dexterity#15 --constitution#11 --speed#30 --ac#12 }} !delability {{ --charid -M0vUlsQ3rtb1LtNN8j7 --deleteall }} !setability {{ --charid -M0vUlsQ3rtb1LtNN8j7 --token --replace --RollInit#\p{%%character_id%%|initiative} --WildShapes#\p{Scenery|WildShapes} --Bite#\amp{template:npcaction} \begattack=1\end \begdamage=1\end \begdmg1flag=1\end \begname=\at{selected|character_name\end} \begrname=Bite\end \begr1=\[1d20+\at{selected|pb}+\at{selected|dexterity_mod}\]\end \begalways=1\end \begr2=\[1d20+\at{selected|pb}+\at{selected|dexterity_mod}\]\end \begdmg1=\[1d4+\at{selected|dexterity_mod}\]\end \begdmg1type=piercing\end \begcrit1=\[1d4\]\end --Keen Smell#\amp{template:npcaction} \begrname=Keen Smell\end \begname=\at{selected|character_name\end} \begdescription=The rat has advantage on Wisdom (Perception) checks that rely on smell.\end --Pack Tactics#\amp{template:npcaction} \begrname=Pack Tactics\end \begname=\at{selected|character_name\end} \begdescription=The rat has advantage on an attack roll against a creature if at least one of the rat's allies is within 5 ft. of the creature and the ally isn't incapacitated.\end }} That's far from the final form which will be made more generic so it functions with any selected token, but it all runs at the same time even though there are four separate API calls. Currently with that character selected I have a token button called WildShapes. When I click that, it whispers the player a chat menu with buttons for Base Form, Giant Rat, and Giant Owl. Clicking Giant Rat will run the commands above which do the following: TokenMod switches the token to refer to the Druid's Wild Shape character sheet (very important so that items won't be present on the Wild Shape and you'll have the original character just as you left it with hit points and abilities untouched when you return to it), changes the token's name, changes the token's side to the one with an image of a Giant Rat, makes it the right size, gives it darkvision out to 60', sets bar1 to a Giant Rat's speed, sets bar2 to a Giant Rat's AC, and rolls hit points for the Giant Rat in bar3. ChatSetAttr writes the Giant Rat's Str, Dex, Con, speed, and AC to the Druid's Wild Shape character sheet. SetAbility deletes all abilities from the Druid's Wild Shape character sheet (cleaning up the abilities of the previous Wild Shape used). SetAbility creates new token abilities so that when you select the token you have clickable buttons on top for rolling initiative (that was really just there as an example for me to follow), bringing up the WildShapes chat menu again (to switch back to Druid form or to another Wild Shape), using the rat's Bite action (which adds the Druid's proficiency bonus), or viewing its Keen Smell and Pack Tactics traits. Eventually it will also add any additional save/skill proficiency which the Wild Shape has that the Druid might lack, but that's a little finicky to figure out. The process to get back to using the original Druid token and character sheet is much simpler and can be handled just by TokenMod: !token-mod {{ --set represents|"Scenery" name|"Scenery" currentside|1 scale|1u light_radius|60 light_dimradius|0 bar1_link|speed bar2_link|ac bar3_link|hp }} The biggest problem with this method is data entry for all the possible forms, but if I can get someone to teach me how to set up a spreadsheet to help with that aspect of things ( like this one by KeithCurtis ), then I could probably just set up macros for all the beasts and elementals in the SRD and share that with people who'd like to use this much more streamlined method. I'll also have to share the code tweak I made to SetAbility, because I had to add a couple more replacers in order to handle the {{ }} format of roll templates. Actually, I can share that here too if you really want to dive into playing around with things yourself, but I *highly* recommend only doing this on duplicates of character sheets rather than any you actually care about. SetAbility courtesy of Jakob (with a couple of added replacers): // setAbility version 1.0 var setAbility = setAbility || (function () { 'use strict'; const version = '1.1', replacers = [ ['[[', /\\\[/g], [']]', /\\\]/g], ['-', /\~/g], ['?', /\\q/g], ['@', /\\at/g], ['%', /\\p/g], ['&', /\\amp/g], ['#', /\\h/g], ['{{', /\\beg/g], ['}}', /\\end/g] ], checkInstall = function () { log(`-=> SetAbility v${version} <=-`); }, isDef = function (value) { return !_.isUndefined(value); }, getWhisperPrefix = function (playerid) { let player = getObj('player', playerid); if (player && player.get('_displayname')) { return '/w "' + player.get('_displayname') + '" '; } else { return '/w GM '; } }, sendChatMessage = function (msg) { sendChat('setAbility', msg, null, { noarchive: true }); }, handleErrors = function (whisper, errors) { if (errors.length) { let output = whisper + '<div style="border:1px solid black;' + `background-color:#FFBABA;padding:3px"><h4>Errors</h4>` + `<p>${errors.join('<br>')}</p></div>`; sendChatMessage(output); errors.splice(0, errors.length); } }, getCharNameById = function (id) { let character = getObj('character', id); return (character) ? character.get('name') : ''; }, htmlReplace = function (str) { let entities = { '<': 'lt', '>': 'gt', "'": '#39', '*': '#42', '@': '#64', '{': '#123', '|': '#124', '}': '#125', '[': '#91', ']': '#93', '_': '#95', '"': 'quot' }; return str.split('') .map(c => (entities[c]) ? ('&' + entities[c] + ';') : c) .join(''); }, processInlinerolls = function (msg) { if (msg['inlinerolls']) { return _.chain(msg.inlinerolls) .reduce(function (m, v, k) { let 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((m, v, k) => m.replace(k, v), msg.content) .value(); } else { return msg.content; } }, getAbilities = function (list, abilityNames, errors, createMissing, deleteMode, getAll) { let abilityNamesUpper = abilityNames.map(x => x.toUpperCase()), allAbilities = {}; list.forEach(charid => { allAbilities[charid] = {}; findObjs({ _type: 'ability', _characterid: charid }).forEach(o => { if (getAll) { allAbilities[charid][o.get('_id')] = o; } else { let nameIndex = abilityNamesUpper.indexOf(o.get('name').toUpperCase()); if (nameIndex !== -1) { allAbilities[charid][abilityNames[nameIndex]] = o; } } }); if (!getAll) { abilityNames.filter(x => !Object.keys(allAbilities[charid]).includes(x)) .forEach(key => { if (createMissing) { allAbilities[charid][key] = createObj('ability', { characterid: charid, name: key }); } else if (!deleteMode) { errors.push(`Missing ability ${key} not created for` + ` character ${getCharNameById(charid)}.`); } }); } }); return allAbilities; }, delayedSetAbilities = function (whisper, list, setting, errors, allAbilities, fillIn, opts) { let cList = [...list], feedback = [], dWork = function (charid) { setCharAbilities(charid, setting, errors, feedback, allAbilities[charid], fillIn, opts); if (cList.length) { _.delay(dWork, 50, cList.shift()); } else { if (!opts.mute) handleErrors(whisper, errors); if (!opts.silent) sendFeedback(whisper, feedback); } } dWork(cList.shift()); }, setCharAbilities = function (charid, setting, errors, feedback, abilities, fillIn, opts) { let charFeedback = {}; Object.entries(abilities).forEach(([abilityName, ability]) => { let newValue = fillIn[abilityName] ? fillInAttrValues(charid, setting[abilityName]) : setting[abilityName]; if (opts.evaluate) { try { let parsed = eval(newValue); if (_.isString(parsed) || _.isFinite(parsed) || _.isBoolean(parsed)) { newValue = parsed.toString(); } } catch (err) { errors.push(`Something went wrong with --evaluate` + ` for the character ${getCharNameById(charid)}.` + ` You were warned. The error message was: ${err}.` + ` Ability ${abilityName} left unchanged.`); return; } } let finalValue = {}; if (opts.token) finalValue.istokenaction = true; if (newValue + '' === newValue) finalValue.action = newValue; charFeedback[abilityName] = newValue; ability.set(finalValue); }); // Feedback if (!opts.silent) { charFeedback = Object.entries(charFeedback).map(([name, value]) => { if (value !== false) return `${name} to ${htmlReplace(value) || '<i>(empty)</i>'}`; else return null; }) .filter(x => !!x); if (charFeedback.length) { feedback.push(`Setting abilities ${charFeedback.join(', ')} for` + ` character ${getCharNameById(charid)}.`); } else if (opts.token) { feedback.push(`Changing token action status for character ${getCharNameById(charid)}.`); } else { feedback.push(`Nothing to do for character ${getCharNameById(charid)}.`); } } return; }, fillInAttrValues = function (charid, expression) { let match = expression.match(/%%(\S.*?)(?:_(max))?%%/), replacer; while (match) { replacer = getAttrByName(charid, match[1], match[2] || 'current') || ''; expression = expression.replace(/%%(\S.*?)(?:_(max))?%%/, replacer); match = expression.match(/%%(\S.*?)(?:_(max))?%%/); } return expression; }, deleteAbilities = function (whisper, allAbilities, silent, deleteall) { let feedback = {}; Object.entries(allAbilities).forEach(([charid, charAbilities]) => { feedback[charid] = []; Object.entries(charAbilities).forEach(([name, ability]) => { feedback[charid].push(deleteall ? ability.get('name') : name); ability.remove(); }); }); if (!silent) sendDeleteFeedback(whisper, feedback); }, // These functions parse the chat input. parseOpts = function (content, hasValue) { // Input: content - string of the form command --opts1 --opts2 value --opts3. // values come separated by whitespace. // hasValue - array of all options which come with a value // Output: object containing key:true if key is not in hasValue. and containing // key:value otherwise return content.replace(/<br\/>\n/g, ' ') .replace(/\s*$/g, '') .replace(/({{(.*?)\s*}}$)/g, '$2') .split(/\s+--/) .slice(1) .reduce((m, arg) => { let kv = arg.split(/\s(.+)/); if (hasValue.includes(kv[0])) { m[kv[0]] = kv[1]; } else { m[arg] = true; } return m; }, {}); }, parseAbilities = function (args, fillIn, replace) { return args.map(str => { let split = str.split('#'); return [split.shift(), split.join('#')]; }) .reduce((m, c) => { if (c[0] && c[1] !== undefined) { let str = c[1]; fillIn[c[0]] = str.search(/%%(\S.*?)(?:_(max))?%%/) !== -1; if (replace) { replacers.forEach(rep => { str = str.replace(rep[1], rep[0]); }); } m[c[0]] = str; } else if (c[0]) { m[c[0]] = false; } return m; }, {}); }, // These functions are used to get a list of character ids from the input, // and check for permissions. checkPermissions = function (list, errors, playerid, isGM) { return list.filter(id => { let character = getObj('character', id); if (character) { let control = character.get('controlledby').split(/,/); if (!(isGM || control.includes('all') || control.includes(playerid))) { errors.push(`Permission error for character ${character.get('name')}.`); return false; } else return true; } else { errors.push(`Invalid character id ${id}.`); return false; } }); }, getIDsFromTokens = function (selected) { return selected.map(obj => getObj('graphic', obj._id)) .filter(x => !!x) .map(token => token.get('represents')) .filter(id => getObj('character', id || '')); }, getIDsFromNames = function (charNames, errors) { return charNames.split(/\s*,\s*/) .map(name => { let character = findObjs({ _type: 'character', name: name }, { caseInsensitive: true })[0]; if (character) { return character.id; } else { errors.push(`No character named ${name} found.`); return null; } }) .filter(x => !!x); }, sendFeedback = function (whisper, feedback) { let output = whisper + '<div style="border:1px solid black;background-color:' + '#FFFFFF;padding:3px;"><h3>Setting abilities</h3><p>' + (feedback.join('<br>') || 'Nothing to do.') + '</p></div>'; sendChatMessage(output); }, sendDeleteFeedback = function (whisper, feedback) { let output = whisper + '<div style="border:1px solid black;background-color:' + '#FFFFFF;padding:3px;"><h3>Deleting abilities</h3><p>'; output += _.chain(feedback) .omit(arr => arr.length === 0) .map(function (arr, charid) { return `Deleting abilities(s) ${arr.join(', ')} for character` + ` ${getCharNameById(charid)}.`; }) .join('<br>') .value() || 'Nothing to do.'; output += '</p></div>'; sendChatMessage(output); }, // Main function, called after chat message input handleInput = function (msg) { if (msg.type !== 'api') { return; } const mode = msg.content.match(/^!(set|del|)ability\b/), whisper = getWhisperPrefix(msg.playerid); if (!mode) return; // Parsing input let charIDList = [], fillIn = {}, errors = []; const hasValue = ['charid', 'name'], optsArray = ['all', 'allgm', 'charid', 'name', 'allplayers', 'sel', 'replace', 'nocreate', 'evaluate', 'silent', 'mute', 'token', 'deleteall' ], opts = parseOpts(processInlinerolls(msg), hasValue), deleteMode = (mode[1] === 'del'), setting = parseAbilities(_.chain(opts) .omit(optsArray) .keys() .value(), fillIn, opts.replace), isGM = msg.playerid === 'API' || playerIsGM(msg.playerid); opts.silent = opts.silent || opts.mute; opts.token = opts.token || false; if (opts.evaluate && !isGM) { if (!opts.mute) handleErrors(whisper, ['The --evaluate option is only available to the GM.']); return; } // Get list of character IDs if (opts.all && isGM) { charIDList = findObjs({ _type: 'character' }).map(c => c.id); } else if (opts.allgm && isGM) { charIDList = findObjs({ _type: 'character' }).filter(c => c.get('controlledby') === '') .map(c => c.id); } else if (opts.allplayers && isGM) { charIDList = findObjs({ _type: 'character' }).filter(c => c.get('controlledby') !== '') .map(c => c.id); } else { (opts.charid) ? charIDList.push(...opts.charid.split(/\s*,\s*/)): null; (opts.name) ? charIDList.push(...getIDsFromNames(opts.name, errors)): null; (opts.sel) ? charIDList.push(...getIDsFromTokens(msg.selected)): null; charIDList = checkPermissions(_.uniq(charIDList), errors, msg.playerid, isGM); } if (_.isEmpty(charIDList)) { errors.push('No target characters. You need to supply one of --all, --allgm, --sel,' + ' --allplayers, --charid, or --name.'); } if (_.isEmpty(setting) && !(deleteMode && opts.deleteall)) { errors.push('No abilities supplied.'); } // Get abilities let allAbilities = getAbilities(charIDList, Object.keys(setting), errors, !opts.nocreate && !deleteMode, deleteMode, deleteMode && opts.deleteall); if (!opts.mute) handleErrors(whisper, errors); // Set or delete abilities if (!(charIDList.length === 0) && (!_.isEmpty(setting) || (deleteMode && opts.deleteall))) { if (deleteMode) { deleteAbilities(whisper, allAbilities, opts.silent, opts.deleteall); } else { delayedSetAbilities(whisper, charIDList, setting, errors, allAbilities, fillIn, _.pick(opts, optsArray)); } } return; }, registerEventHandlers = function () { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready', function () { 'use strict'; setAbility.CheckInstall(); setAbility.RegisterEventHandlers(); }); Overall though, I'm very glad I saw this post of yours because it got me working on this again and getting very close to something I've wanted for a long time.