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 .
×
Advertisement Create a free account

[Script Update] TokenMod -- An interface to adjusting properties of a token from a macro or the chat area.

1508085819
GM Matt
Sheet Author
This is amazing! Can I ask for a little more help? Here is something I'll need for sure: (1) I need a way to include the equivalent of @{target|x} in the math (both from status tokens and the character sheet) A couple of things that would help: (1) Can you give me an idea about how to hard code the negative statuses into the jscript so that I don't have to use --torg-set every session? (2) It would help (but isn't necessary) if I could associate each status icon with a word describing the penalty (example: tread = "run penalty"). That text could either show up in chat along with the number and icon, or it could be a tool tip. Thanks again, Aaron.
1508089168
The Aaron
Forum Champion
API Scripter
Regarding the something you need (1) -- Can you give a thorough example of what you mean?&nbsp; You could already do this: !torg 0d0+(@{target|Foo} [Foo]) ==SomeAttr --Including some extra something from a target creature other than the selected ones. This would use the same target for all selected tokens' rolls. Regarding things that would help: 1)&nbsp; It already does this.&nbsp; You only need to run it again if you need to reset the statuses that are negative. 2) That's definitely doable, probably would need to pass the status and the name, something like: !torg-name --blue|Frost Damage --red|Fire Damage Ok. summary: !torg-set -- you can no use this with nothing selected to tell you what is configured for negatives !torg-set !torg-name -- use this to name statuses !torg-name --blue|Frost --green|Poison --edge-crack|Break all the Things Use it with no arguments to see the list.&nbsp; Don't supply a name to something to remove it: !torg-name --blue !torg -- This is unchanged, except now it will include names for things if there are names set: Give this one a try: on('ready',()=&gt;{ &nbsp; &nbsp; const version = '0.1.1', &nbsp; &nbsp; &nbsp; &nbsp; lastUpdate = 1508088657, &nbsp; &nbsp; &nbsp; &nbsp; schemaVersion = 0.2; &nbsp; &nbsp; const apiCmd = /^!torg\b($|\s+)/i; &nbsp; &nbsp; const apiCmdSet =/^!torg-set\b/i; &nbsp; &nbsp; const apiCmdName =/^!torg-name\b/i; &nbsp; &nbsp; const statuses = [ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'red', 'blue', 'green', 'brown', 'purple', 'pink', 'yellow', // 0-6 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'skull', 'sleepy', 'half-heart', 'half-haze', 'interdiction', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'snail', 'lightning-helix', 'spanner', 'chained-heart', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'chemical-bolt', 'death-zone', 'drink-me', 'edge-crack', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'ninja-mask', 'stopwatch', 'fishing-net', 'overdrive', 'strong', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'fist', 'padlock', 'three-leaves', 'fluffy-wing', 'pummeled', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'tread', 'arrowed', 'aura', 'back-pain', 'black-flag', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'bleeding-eye', 'bolt-shield', 'broken-heart', 'cobweb', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'broken-shield', 'flying-flag', 'radioactive', 'trophy', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'broken-skull', 'frozen-orb', 'rolling-bomb', 'white-tower', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'grab', 'screaming', 'grenade', 'sentry-gun', 'all-for-one', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'angel-outfit', 'archery-target' &nbsp; &nbsp; &nbsp; &nbsp; ]; &nbsp; &nbsp; const statusColormap = ['#C91010', '#1076c9', '#2fc910', '#c97310', '#9510c9', '#eb75e1', '#e5eb75']; &nbsp; &nbsp; const getStatusIconByIndex = (idx) =&gt; (idx&lt;7) &nbsp; &nbsp; &nbsp; &nbsp; ? `&lt;div style="width: 1em; height: 1em; border-radius:20px; display:inline-block; margin: 0; border:0; cursor: pointer;background-color: ${statusColormap[idx]}"&gt;&lt;/div&gt;` &nbsp; &nbsp; &nbsp; &nbsp; :`&lt;div style="width: 1em; height: 1em; display:inline-block; margin: 0; border:0; cursor: pointer;padding:0;background-image: url('<a href="https://app.roll20.net/images/statussheet.png');background-repeat:no-repeat;background-position" rel="nofollow">https://app.roll20.net/images/statussheet.png');background-repeat:no-repeat;background-position</a>: ${((-(34/24))*(idx-7))}em 0;background-size:auto 100%;"&gt;&lt;/div&gt;` &nbsp; &nbsp; &nbsp; &nbsp; ; &nbsp; &nbsp; const getStatusIcon = (status) =&gt; getStatusIconByIndex(_.indexOf(statuses,status)); &nbsp; &nbsp; const outer = (contents) =&gt; `&lt;div style="margin-bottom: .3em;border:1px solid #999;background-color: #ffe;padding: .2em; border-radius:.2em;"&gt;${contents}&lt;div style="clear:both;"&gt;&lt;/div&gt;&lt;/div&gt;`; &nbsp; &nbsp; const inner = (contents) =&gt; `&lt;div&gt;${contents}&lt;/div&gt;`; &nbsp; &nbsp; const result = (contents) =&gt; `&lt;div style="display:inline-block;font-size: 1.5em; border: 1px solid #999; background-color: #fef; font-weight: bold; border-radius: .2em; padding: .4em .2em;margin-right: .2em;"&gt;${contents}&lt;/div&gt;`; &nbsp; &nbsp; const icon = (img)=&gt;`&lt;img style="max-height:2.6em;max-width:4em; float:left;" src="${img}"&gt;`; &nbsp; &nbsp; const rollFmt = (contents) =&gt;`[[${contents}]]`.replace(/\[\[\s+/,'[[').replace(/\[\[\s+\[\[/,'[[[['); &nbsp; &nbsp; const attrRoll = (attrs) =&gt; attrs.map(a=&gt;`${parseInt(a.value)||0} [${a.attr}]`).join('+'); &nbsp; &nbsp; const attrMesg = (attrs) =&gt; attrs.map(a=&gt;` &lt;span style="white-space: nowrap;display:inline-block;border: 1px solid #999;background-color:#eff;border-radius:.4em;padding: .1em .4em;"&gt;${a.value} ${a.attr}&lt;/span&gt; `).join(''); &nbsp; &nbsp; const attrTotal = (attrs) =&gt;attrs.reduce((m,a)=&gt;m+parseFloat(a.value)||0,0); &nbsp; &nbsp; const statusBubble = (contents) =&gt;`&lt;span style="white-space: nowrap;display:inline-block;border: 1px solid #999;background-color:#eff;border-radius:.4em;padding: .1em .4em;"&gt;${contents}&lt;/span&gt;`; &nbsp; &nbsp; const statusName = (status) =&gt; state.TorgStatus.statusNames[status]||''; &nbsp; &nbsp; const parseStatuses = (statuses) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; let s = statuses.split(/,/).map((sb)=&gt;({status:sb.split(/@/)[0],num:sb.split(/@/)[1]||0})); &nbsp; &nbsp; &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; roll:s.map(st=&gt;`( ${state.TorgStatus.negativeStatuses.includes(st.status) ? '-' :''}${st.num} [${st.status}])`).join('+'), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mesg:s.map(st=&gt;` ${state.TorgStatus.negativeStatuses.includes(st.status) ? '-' :'+'}${st.num} ${getStatusIcon(st.status)}${statusName(st.status)}`).map(statusBubble).join(''), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; total:s.reduce((m,st)=&gt;m+(state.TorgStatus.negativeStatuses.includes(st.status) ? -1 : 1) * st.num, 0) &nbsp; &nbsp; &nbsp; &nbsp; }; &nbsp; &nbsp; }; &nbsp; &nbsp; const checkInstall = () =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; log('-=&gt; TorgStatus v'+version+' &lt;=-&nbsp; ['+(new Date(lastUpdate*1000))+']'); &nbsp; &nbsp; &nbsp; &nbsp; if( ! _.has(state,'TorgStatus') || state.TorgStatus.version !== schemaVersion) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log('&nbsp; &gt; Updating Schema to v'+schemaVersion+' &lt;'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; switch(state.TorgStatus && state.TorgStatus.version) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 0.0: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 0.1: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.TorgStatus.statusNames = {}; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* falls through */ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'UpdateSchemaVersion': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.TorgStatus.version = schemaVersion; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.TorgStatus = { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; version: schemaVersion, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; negativeStatuses: [], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; statusNames: {} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; on('chat:message',(orig_msg)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; if('api' === orig_msg.type) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(apiCmd.test(orig_msg.content) ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let msg=_.clone(orig_msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(_.has(msg,'inlinerolls')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.content = _.chain(msg.inlinerolls) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce(function(m,v,k){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var ti=_.reduce(v.results.rolls,function(m2,v2){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(_.has(v2,'table')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m2.push(_.reduce(v2.results,function(m3,v3){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m3.push(v3.tableItem.name); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m3; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },[]).join(', ')); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m2; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },[]).join(', '); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },{}) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce(function(m,v,k){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m.replace(k,v); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },msg.content) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .value(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let mesg = msg.content.split(/\s+--/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let attrs = mesg.shift().split(/\s+==/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let roll = attrs.shift().replace(apiCmd,'').trim()||''; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attrs=attrs.reduce((m,a)=&gt;_.union(m,a.split(/\s+/)),[]).map(a=&gt;a.toLowerCase()).reduce((m,a)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let p=a.split(/\|/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let n=p[0]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let f=(p[1]==='max'?'max':'current'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return Object.assign(m,{[n]:(m[n]||[]).concat([f])}); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },{}); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mesg = mesg.join(' '); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.chain(msg.selected) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map((o)=&gt;getObj('graphic',o._id)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject(_.isUndefined) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map((o)=&gt;({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t: o, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a: findObjs({type:'attribute',characterid:o.get('represents')}) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter((a)=&gt;Object.keys(attrs).includes(a.get('name').toLowerCase())) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce((m,a)=&gt;m.concat(attrs[a.get('name').toLowerCase()].map(f=&gt;({attr:`${a.get('name')}${f==='max'?'|max':''}`,value:a.get(f)}))),[]), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b: parseStatuses(o.get('statusmarkers')) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map((o)=&gt;outer( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; icon(o.t.get('imgsrc'))+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inner( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result(roll.length &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ? rollFmt(`${roll}+[attributes:](${attrRoll(o.a)})+[status:](${o.b.roll})`)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : (attrTotal(o.a)+o.b.total))+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (mesg.length ? mesg : '') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; `&lt;div&gt;${attrMesg(o.a)}${o.b.mesg}&lt;/div&gt;` &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .tap(m=&gt;{try{sendChat('',m.join(''));}catch(e){log(m);}}) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if(apiCmdSet.test(orig_msg.content) && playerIsGM(orig_msg.playerid) ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let who=(getObj('player',orig_msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(orig_msg.selected){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _.chain(orig_msg.selected) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map((o)=&gt;getObj('graphic',o._id)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reject(_.isUndefined) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce((m,o)=&gt;_.union(m,o.get('statusmarkers').split(/@\d,|@\d|,/)),[]) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(s=&gt;s.length&&s!=='dead') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .tap(s=&gt;state.TorgStatus.negativeStatuses=s) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('',`/w "${who}" Negative Statuses: ${_.map(state.TorgStatus.negativeStatuses,getStatusIcon).join('')}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if(apiCmdName.test(orig_msg.content) && playerIsGM(orig_msg.playerid) ){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let who=(getObj('player',orig_msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let msg=_.clone(orig_msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(_.has(msg,'inlinerolls')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.content = _.chain(msg.inlinerolls) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce(function(m,v,k){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var ti=_.reduce(v.results.rolls,function(m2,v2){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(_.has(v2,'table')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m2.push(_.reduce(v2.results,function(m3,v3){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m3.push(v3.tableItem.name); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m3; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },[]).join(', ')); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m2; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },[]).join(', '); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },{}) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .reduce(function(m,v,k){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return m.replace(k,v); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },msg.content) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .value(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.content &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .split(/\s+--/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .slice(1) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map(s=&gt;({status:s.split(/\|/)[0],name:s.split(/\|/)[1]})) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(s=&gt;statuses.includes(s.status)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .forEach(s=&gt;s.name ? state.TorgStatus.statusNames[s.status]=s.name : delete state.TorgStatus.statusNames[s.status] ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('',`/w "${who}" Status Names: ${Object.keys(state.TorgStatus.statusNames).map((k)=&gt;`${getStatusIcon(k)}:${state.TorgStatus.statusNames[k]}`).map(statusBubble).join('')}`); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); &nbsp; &nbsp; checkInstall(); });
1508092709
GM Matt
Sheet Author
Almost there. I am trying: !torg 0d0+(@{target|Dodge} [Dodge]) ==wnd --difficulty. Modifiers: ...but there is no output to chat. If I change it to: !torg ==wnd --difficulty. Modifiers: I get ...which is what I'm looking for, except I don't have the target's Dodge attribute in the calculus. (Note: I don't have values attached to the status icons in this example, which I would in an actual game).
1508103229
The Aaron
Forum Champion
API Scripter
Ah. I see what's happening.&nbsp; This is a problem that has happened off and on for the last 4 years.&nbsp; Basically, when you use @{target}, it causes everything you have selected to be de-selected while it picks the target, then the API message is issued, then the tokens are reselected: There isn't really a way I can fix that and still let you use @{target} to collect info and the selected creatures to present it.
1508105978
GM Matt
Sheet Author
Aaah. Well, I can still make it work. First, click on the target and get this: Then, on attacker for this: (will correct this macro to read "modifier") And the attack roll: Handy for when there are a lot of these factors at play. Otherwise, can just do the math in your head. On behalf of myself and the rest of the Torg community on R20, thanks so much for taking the time to put this together, Aaron! I expect this is going to be used a lot.&nbsp; - Matt - Matt
1508106318
The Aaron
Forum Champion
API Scripter
Cool! &nbsp;Guess I should give it its own thread.&nbsp;
1509237390
If I'm planning to use a creature that alters its appearance during combat, which I would do by simply stacking 4 tokens on top of each other and bringing to the front the one I want to use for that particular round, can this be done with your script? Or even better, could this be done via a regular macro?
1509243211
Matt R. said: If I'm planning to use a creature that alters its appearance during combat, which I would do by simply stacking 4 tokens on top of each other and bringing to the front the one I want to use for that particular round, can this be done with your script? Or even better, could this be done via a regular macro? You could use a rollable table to change the tokens
1509244151
Craven said: You could use a rollable table to change the tokens Indeed, that's what someone else suggested me also, and after trying it out (I didn't know about such things), it turns out it's near perfect as far as what I needed, thanks a lot!
1510386287
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Feature request: Changing faces on rollable table tokens. I know this can be done with ChangeTokenImage, but that script requires the token to be selected; it has no --ids option. This would be useful for manipulating things on the map layer like doors or pits without requiring traveling to the map layer, or leaving them on the token layer where they can be accidentally selected. If it would be easier to add --ids to&nbsp;ChangeTokenImage, that would be cool, too.
1510437224
The Aaron
Forum Champion
API Scripter
Ok. Can probably do that pretty easily.&nbsp;
1510445803
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Cool, thanks!
1510507871
Whats The Arron working on? Will it make life easier? I can't wait.
Is there a way to target all tokens controlled by a single player? Context: I wish to flip light_hassight for multiple tokens controlled by the same player. Can I avoid having to write all the ids for the tokens into the macro, as I would like the macro to work even if more tokens are placed under the control of the player.
1512389936
The Aaron
Forum Champion
API Scripter
There isn’t currently. The closest thing is targeting all tokens for a given character by passing a character id (or more than one) to --ids
Ok. Thank you for the quick answere.
1512431824
The Aaron
Forum Champion
API Scripter
Update v0.8.36 -- Updated the help system and added/expanded 3 section: Full details in the first post, but the summary is: colors -- You can specify them in more formats now, (HTML, RGB, HSV), you can add/subtract/multiply them, and you can toggle them on/off with !. imgsrc -- You can copy the image from other tokens, build Multi-Sided Tokens, add/remove sides, etc. side -- You can set the side for a multi-sided token, with or without wrapping, using math, etc. Thanks keithcurtis and Lady Victoria for the suggestions! Bonus, I cleaned up the help and it should be easier to use status markers now because of the improved reference:
1512445169
The Aaron said: Update v0.8.36 -- Updated the help system and added/expanded 3 section: Full details in the first post, but the summary is: imgsrc -- You can copy the image from other tokens, build Multi-Sided Tokens, add/remove sides, etc. imgsrc -- You can copy the image from other tokens, build Multi-Sided Tokens, add/remove sides, etc. So how does this work?
1512445346
The Aaron
Forum Champion
API Scripter
Loads of examples in the first post and the in game help, but basically: !token-mod --set imgsrc|@{target|token_id} --ids @{selected|token_id}
1512451302
I am sorry i didn't know you update the start of the post with the new features.
1512473921

Edited 1512473958
Joe
Pro
The Aaron said: Update v0.8.36 -- Updated the help system and added/expanded 3 section: &nbsp; &nbsp; [...] side -- You can set the side for a multi-sided token, with or without wrapping, using math, etc. Bonus, I cleaned up the help and it should be easier to use status markers now because of the improved reference: Yay! &nbsp;I’m really happy to see the “side” option, and adding the images to the status icon names will be very helpful as well, so thank you!
1512474774
Joe
Pro
Hi Aaron, Your help text in the first post is excellent and thorough as usual, thank you again. I did notice that you say when removing images the token can default back to image 0. Is that a typo, because all of your other multi-image references are 1-based indexing? &nbsp;Or am I misunderstanding? Also, the boxed text example following “Using = with this syntax will set the current side to the last added image” does not add the referenced “=“.
1512479599

Edited 1512479634
The Aaron
Forum Champion
API Scripter
Ah yes! Both typos. It should say 1. Internally, it’s a zero-biased array, but that’s confusing to non-programmers so I do the math internally to make it start at 1. I’ll fix that = as well, just omitted it from the example.&nbsp;
1512479668
The Aaron
Forum Champion
API Scripter
Craven said: I am sorry i didn't know you update the start of the post with the new features. No biggy! =D
1512487664

Edited 1512487884
Hey,&nbsp; great work, I'm loving your script! But I'm having a little trouble. I wanna set all of my players tokens to say 0 vision. I got that no problem. Now the problem is that I don't know how to set their vision back to normal as some have darkvision and others are human. (assuming dnd 5e here). The easiest solution would be to replace all selected tokens with the defaulttoken values, saved in their respective character sheets. Or I guess I could write the macro so that when I select all their tokens it would set player 1's token to 60/0 vision and player 2's to 0/0 vision? How would I write either of those macros?&nbsp; I know it should start with:&nbsp; !token-mod --set light_radius|150 light_dimradius|70&nbsp; ... maybe? Sorry. I think my problem is that I don't understand what&nbsp;@{target|token_id} means and how to use it, and where in the command.
1512488440
The Aaron
Forum Champion
API Scripter
No problem.&nbsp; @{target|token_id} is a string of text that when sent to chat will cause Roll20 to prompt you to select a token.&nbsp; It will then replace it in the command with the token_id of the token you selected. All tokens have a unique ID called the token_id that uniquely identifies them to Roll20.&nbsp; Characters likewise have that ID and it's called by character_id.&nbsp; You can collect the token_id by either using @{target|token_id} and selecting a token, or by using @{selected|token_id}.&nbsp; With @{target}, you can specify a label and collect multiple tokens' ids:&nbsp; @{target|first|token_id} @{target|second|token_id} etc&nbsp; Character ids can be collected in the same manner, and also by using the character's full name:&nbsp; @{Bob the Slayer|character_id} @{Tommy two toes|character_id} TokenMod can be passed either token or character ids using the --ids argument.&nbsp; If you pass a character_id, TokenMod will find all tokens that represent that character and make changes to them. The practical upshot of all that is you can probably write something like this: !token-mod --set light_radius|150 light_dimradius|70&nbsp;--ids @{Timmy Blades|character_id} @{Sorcerer Pete|character_id} and adjust all those characters tokens without selecting them.
1512488696
ohhhh that makes so much more sense now, "character_id" is a so much smarter solution. Thank you very much! got it all working now. thank you! :)
1512488881
The Aaron
Forum Champion
API Scripter
You'll probably want to add --ignore-selected to the command if you aren't going to select things.&nbsp; That will prevent you from accidentally changing the site of any tokens that happen to be selected when you hit the macro. =D Here's the macro I use for sight: !token-mod --set ?{Vision|Torch, light_radius#40 light_dimradius#20 light_hassight#yes light_angle#360 light_otherplayers#yes|Hooded Lantern, light_radius#60 light_dimradius#30 light_hassight#yes light_angle#360 light_otherplayers#yes|Bullseye Lantern, light_radius#120 light_dimradius#60 light_angle#60 light_hassight#yes light_otherplayers#yes|Lamp, light_radius#30 light_dimradius#15 light_hassight#yes light_angle#360 light_otherplayers#yes|Candle, light_radius#5 light_dimradius#=0 light_hassight#yes light_angle#360 light_otherplayers#yes|Darkvision, light_radius#60 light_dimradius#=-5 light_hassight#yes light_angle#360 light_otherplayers#no|Darkvision (90'), light_radius#90 light_dimradius#=-5 light_hassight#yes light_angle#360 light_otherplayers#no|Warlock Devil's Sight, light_radius#120 light_dimradius#=120 light_hassight#yes light_angle#360 light_otherplayers#no|No light source(Dusk), light_radius#120 light_dimradius#=-5 light_hassight#yes light_angle#360 light_otherplayers#no|Fog, light_radius#200 light_dimradius#=5 light_hassight#yes light_angle#360 light_otherplayers#no|No light source, light_radius#5 light_dimradius#=-5 light_hassight#yes light_angle#360 light_otherplayers#no|Blinded, light_hassight#no light_angle#360 light_otherplayers#no} It prompts for the type of sight and then changes all the selected tokens.&nbsp; You could add --ignore-selected and --ids @{ some characters } to it if you want to be prompted for what to light.&nbsp; You could even add the characters as a drop down to change specific characters without having to track them down on the page.
1512490648
uhh, I'll add that macro for when I have more complex needs, thank you. Before I went pro I just had tokens made like torch, light and lantern and stuff so the players could already drag those out themselves. I set those up with default tokens controlled by everyone. But your macro will be used :) I also added the --ignore-selected to my macros. thanks again for the super quick reply. have a great day!&nbsp;
1512491057
The Aaron
Forum Champion
API Scripter
no problem!&nbsp; =D
1512496686
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
This looks like&nbsp; a great update, the Aaron! Thanks! This is coming closer and closer to being the one-stop Swiss Army Script.
1512497484
The Aaron
Forum Champion
API Scripter
keithcurtis said: This looks like&nbsp; a great update, the Aaron! Thanks! This is coming closer and closer to being the one-stop Swiss Army Script. What are next on the list? =D~
1512514422
Ravenknight
KS Backer
Side switching is extremly helpful to me. Thanks Aaron. :)
1512516387
The Aaron
Forum Champion
API Scripter
No problem!
1512517324
So I'm getting an error in my campaign when I start my campaign.&nbsp; I disabled and deleted the script and reloaded but still getting the same error: Error: Firebase.update failed: First argument contains NaN in property 'light_dimradius' Error: Firebase.update failed: First argument contains NaN in property 'light_dimradius' at Ba (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:9:186) at Ba (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:10:207) at Aa (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:8:462) at Ea (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:10:403) at J.update (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:146:318) at TrackedObj._doSave (/home/node/d20-api-server/api.js:766:27) at Timeout.later [as _onTimeout] (/home/node/d20-api-server/node_modules/underscore/underscore.js:828:25) at ontimeout (timers.js:380:14) at tryOnTimeout (timers.js:244:5) at Timer.listOnTimeout (timers.js:214:5) Code issue or user error?
1512518009
The Aaron
Forum Champion
API Scripter
Do you know what command you ran? &nbsp;Setting light_dimradius apparently resulted in something that isn’t a number. I didn’t touch that code on this update, so probably a pre-existing issue if it happened from TokenMod. Do you know which token it is you operated on? &nbsp;You can load it in the GUI and change it. Also, does that error come up constantly?
1512518129
It comes up on the script screen when I try to start up the sandbox.&nbsp; If I'm in the campaign itself and I use the Torch mod it just doesn't do anything.&nbsp; No errors on that side.&nbsp;&nbsp;
1512518221
The Aaron
Forum Champion
API Scripter
If you wanna PM me a join link, I can come check it out
1512518730
I'm getting ready to run the game (and thankfully it's not crucial for running the session), but I'll drop you the link when we finish up.&nbsp; Thanks!
1512518758
The Aaron
Forum Champion
API Scripter
No problem, just let me know.
1512527933

Edited 1512528056
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Am I doing something wrong? If I create the command: !token-mod --set width|[[70*22]] height|[[70*14]] --ids @{Scenes|character_id} It does indeed set the size of the "Scenes" token, but also whatever tokens I happen to have selected. It might also be doing it for the face command I have set, but the Scenes token is the only multi-sided token. Never mind: I found the&nbsp;--ignore-selected option.
1512528425
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
This works beautifully. I now have a multisided token for all my Theatre of the Mind scenes that I can keep on the map layer. I can swap scenes and mood music with the push of a menu button without having the thing selected (and thus accidentally moved)
1512529242
The Aaron
Forum Champion
API Scripter
Sweet!! &nbsp; If Roll20 ever fixes the bug that causes the use of @{target} to prevent the API from getting the selected list, that option will become a bunch more important! =D
1512529985

Edited 1512530327
Ravenknight said: Side switching is extremly helpful to me. Thanks Aaron. :) What is side switching? What are multi-sided tokens? Is that when you create a table to generate different tokens? Sorry just lost on the function of the new update.
1512530229
The Aaron
Forum Champion
API Scripter
When you have a Rollable Table Token, it has multiple sides. Normally, you right click the token and drag a slider about to pick the side. This lets you set it with an API command, and do it on multiple tokens at a time. In KeithCurtis’s case, it lets him do it without selecting it at all.&nbsp;
1512530434
So i could have a floor token and under that a pit trap and use the macro to change the floor to trap on the map layer without going to that layer?
1512530566
The Aaron
Forum Champion
API Scripter
You could! You’d need to know the token ID or have it represent a character and use the character id.&nbsp;
1512531358

Edited 1512531380
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Craven said: So i could have a floor token and under that a pit trap and use the macro to change the floor to trap on the map layer without going to that layer? That's a very good idea! You could even have a menu macro for a particular page that handles any number of object images. Pit traps, doors, cave-ins, and so on. Multisided tokens are useful also for lycanthropes and other shapeshifters, similar tokens that are kitted differently (goblin archers, v. goblin swordsmen), and mimics. I use one that has spell effects on different sides (fireball, darkness, etc.) With one command it changes the size and face of the token to duplicate the effect and area of the spell.
1512531693
The Aaron
Forum Champion
API Scripter
I’m looking forward to seeming what people do with being able to build multi-sided tokens without the Rollable Table. Would be nice if the API could access Marketplace images.&nbsp;
1512537492
keithcurtis said: Craven said: So i could have a floor token and under that a pit trap and use the macro to change the floor to trap on the map layer without going to that layer? That's a very good idea! You could even have a menu macro for a particular page that handles any number of object images. Pit traps, doors, cave-ins, and so on. Multisided tokens are useful also for lycanthropes and other shapeshifters, similar tokens that are kitted differently (goblin archers, v. goblin swordsmen), and mimics. I use one that has spell effects on different sides (fireball, darkness, etc.) With one command it changes the size and face of the token to duplicate the effect and area of the spell. Keith let me know what you come up with and post some screenshot. Like to see how this works.