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: