The Aaron said: Ah, that makes the script a bit more complicated. One caveat is that in order for the Token Action button to appear or disappear when the setting is changed, you have to deselect the token. I whipped up a little script to do this. Here's how it works: For the bard, assuming an ability named Bard Inspiration , they just need to run something like this: !toggle-ability --enable --name bard inspiration --ids @{target|token_id} This will set istokenaction to true on all the matching abilities. The ability name just needs to have the same contiguous set of letters (case insensitive), so "bard insp" or "bardinspiration" or "inspiration" would all match the ability name. (if you're too in-specific, you'll end up matching a bunch, heads up). You can provide as many ids as you like, either token ids (as above) or character ids or a mix of both: !toggle-ability --enable --name granted --ids @{Grog the Great|character_id} @{Bob the Slayer|character_id} For the abilities, you can add: !toggle-ability and save the ability. The script will find that and update it to say something like: !toggle-ability --disable --ids -KQLK4d2vD1OvwyUldZT which will cause the ability have istokenaction set to false when it gets used. Here's the code: on('ready',()=>{
const keyFormat = (text) => (text && text.toLowerCase().replace(/\s+/g,'')) || undefined;
const matchKey = (keys,subject) => subject && !_.isUndefined(_.find(keys,(o)=>(-1 !== subject.indexOf(o))));
const isString = (s)=>'string'===typeof s || s instanceof String;
const parse = (txt) => txt.split(/\s+--/).slice(1).reduce((m,a)=>{
let a2 = a.split(/\s+/);
let key = a2[0].toLowerCase();
switch(key){
case 'enable':
case 'disable':
m[key]=true;
break;
case 'name':
m[key] = a.slice(key.length+1);
m[`${key}_key`] = keyFormat(m[key]);
break;
case 'ids':
m[key] = [...(m[key]||[]),...a2.slice(1)];
break;
}
return m;
},{});
const s = {
outer: `border:1px solid #999; border-radius: .5em; padding: .1em .3em; background-color: #ccc;`,
err: `color: #990000;font-weight:bold;`,
warn: `color: #ffc107;font-weight:bold;display:inline-block;background:#000;padding:.1em .3em;`
};
const f = {
err: (m)=>`<span style="${s.err}">${m}</span>`,
warn: (m)=>`<span style="${s.warn}">${m}</span>`,
code: (m)=>`<code>${m}</code>`
};
const ids2chars = (ids)=> (ids || [])
.reduce((m,id) => [...m, {id, token: getObj('graphic',id)}] ,[])
.reduce((m,o)=> [...m,(undefined === o.token ? o.id : o.token.get('represents'))],[])
.map(id => getObj('character',id))
.filter(g=>undefined !== g)
;
const findAbilities = (name, cs) => cs
.map(c=>c.id)
.map(id => findObjs({type: 'ability', characterid: id}).filter(a=>matchKey([name],keyFormat(a.get('name'))))[0])
.filter(o=>undefined !==o);
const say = (who,msg) => sendChat('',`/w "${who}" <div style="${s.outer}">${msg}</div>`);
const leftDiff = (A,B) => B.filter(i => !A.includes(i));
on('change:ability:action',(obj)=>{
let a = obj.get('action');
if(/^!toggle-ability\b\s*$/img.test(a)){
obj.set({action: a.replace(/^!toggle-ability\b\s*$/img,`!toggle-ability --disable --ids ${obj.id}`)});
}
});
on('chat:message',(msg)=>{
if('api'===msg.type && /^!toggle-ability\b/i.test(msg.content)){
let cmd = parse(msg.content);
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
if(cmd.enable === cmd.disable){
say(who,`${f.err('Error:')} You must specify only one of either ${f.code('--enable')} or ${f.code('--disable')}.`);
} else if(cmd.enable){
if(isString(cmd.name)){
if(Array.isArray(cmd.ids)){
let cs = ids2chars(cmd.ids);
let missing = leftDiff(cmd.ids,cs.map(c=>c.id));
if(missing.length){
say(who,`${f.warn('Notice:')} No characters found for ids: ${missing.join(', ')}`);
}
let abs = findAbilities(cmd.name_key,cs);
let missing2 = leftDiff(abs.map(a=>a.get('characterid')),cs.map(c=>c.id));
if(missing2.length){
say(who,`${f.warn('Notice:')} Characters without the ${cmd.name} ability: ${cs.filter(c=>missing2.includes(c.id)).map(c=>c.get('name')).join(', ')}`);
}
abs.forEach(a=>a.set({
istokenaction: true
}));
} else {
say(who,`${f.err('Error:')} No IDs specified, use ${f.code('--ids')} to add token or character ids to find abilities.`);
}
} else {
say(who,`${f.err('Error:')}: No Name specified, use ${f.code('--name')} to add a name (or name fragment) for the ability.`);
}
} else if(cmd.disable){
if(Array.isArray(cmd.ids)){
let abs = cmd.ids.map(id => getObj('ability',id)).filter(o=>undefined !== o);
let missing = leftDiff(cmd.ids, abs.map(a=>a.id));
if(missing.length){
say(who,`${f.warn('Notice:')} No Abilities found for ids: ${missing.join(', ')}`);
}
abs.forEach(a=>a.set({
istokenaction: false
}));
} else {
say(who,`${f.err('Error:')} No IDs specified, use ${f.code('--ids')} to add the ability ids to hide.`);
}
}
}
});
});
There are probably a bunch of enhancements you could make to this (like checking all the abilities at start up to make sure the generated code is still valid, such as when you clone a character and end up with a new ability id), but this is a pretty functional start. As for learning the API, these are some good places to start: <a href="https://app.roll20.net/forum/post/66605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern" rel="nofollow">https://app.roll20.net/forum/post/66605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern</a> <a href="https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1</a> <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a> Davyd said: Omg that's amazing. I didn't expect you to actually make something. Thank you so much! Quick followup, is there a way for token-mod and toggle-ability to target off the same target query? This is what I have so far !token-mod --ids @{target|Select creature to inspire|token_id} --set statusmarkers#+Bardic-Inspiration !toggle-ability --enable --name bardic inspiration --ids @{target|token_id} But it prompts too target queries and just having one would be ideal Also I keep getting Notice: No characters found for ids: -Lc2_PJWU8K1PoHtnahZ Update: I tried solve the multiselection issue using another one of your scripts Aaron, !gsa. I would store the targets ID as an attribute on the bard, then call that for targeting. The problem is that it seems to resolve the whole macro before updating the stored ID, meaning the previously selected character gets inspried, Any suggestions? This might be one to take outside of Token-Mod; creating it's own forum post - bc this is really good and deserves its own post! (Why did I even need to enter that redundancy? It's TheAaron's code! Of course it good!)