Ok, give this a shot...
Commands
- !apply-change --<argument>[param]|[value] ... ::= Applies a change to the selected or specified tokens.
- --bar ::= This specified which bar to apply the change to. ex: --bar|1
- --amount ::= This is an amount to change by. --amount|-10. You can supply tags to an amount like by appending a comma separated list in brackets
- --amount[fire]|-10
- --amount[force,piercing]|-3
- --damage ::= This is just like amount, but multiplied by -1. --amount|-10 is the same as --damage|10
- --mod ::= This is a modifier rule. It takes the form --mod[<rule>[,<rule>,...]]|<operation><number>
- <rule> can be is a <type>:<value> where <type> is one of the following strings. It can optionally be prefaced by a ! to negate the meaning.
- status ::= the <value> will be one of the status marker names (same ones used by TokenMod)
- tag ::= the <value> will be one of the tags you supplied to a --amount or --damage argument
- <operation> must be one of the following characters. It can optionally be followed by a number in brackets to denote the number of decimal places to retain (defaults to floor of the value).
- * -- multiple the amount by the <number>
- - -- subtract the <number> from the a amount
- + -- add the <number> to the amount
- / -- divide the amount by the <number>
- = -- replace the amount with the <number>
- % -- replace the amount with the modules by <number>
- --ids ::= a list of ids like TokenMod takes. --ids <id1> <id2> <id3>
Ok. That probably sounds complicated. I'll give some examples that should make this easier:
Simplest use, applying damage to all selected tokens:
!apply-change --bar|3 --damage|[[3d6]]
Half damage if they saved, denoted by a red status marker:
!apply-change {{
--bar|3
--damage|[[6d6]]
--mod[status:red]|*.5
}}
Using a tag, if they creatures with brown mark are resistant to fire:
!apply-change {{
--bar|3
--damage[fire]|[[6d6]]
--mod[tag:fire,status:brown]|*.5
}}
Resistant and saved:
!apply-change {{
--bar|3
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*.5
--mod[status:red]|*.5
}}
How about more than one damage type:
!apply-change {{
--bar|3
--damage[acid]|[[2d4]]
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*.5
--mod[status:red]|*.5
}}
And maybe a green status demarks immunity to acid:
!apply-change {{
--bar|3
--damage[acid]|[[2d4]]
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*.5
--mod[status:red]|*.5
--mod[status:green]|=0
}}
And possibly the half damage save doesn't apply to acid:
!apply-change {{
--bar|3
--damage[acid]|[[2d4]]
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*.5
--mod[!tag:acid,status:red]|*.5
--mod[status:green]|=0
}}
For completeness, maybe you count HP to 2 decimal places:
!apply-change {{
--bar|3
--damage[acid]|[[2d4]]
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*[2].5
--mod[!tag:acid,status:red]|*[2].5
--mod[status:green]|=0
}}
Or possibly on a save, the acid damage heals:
!apply-change {{
--bar|3
--damage[acid]|[[2d4]]
--damage[fire]|[[6d6]]
--mod[tag:brown,status:brown]|*[2].5
--mod[!tag:acid,status:red]|*[2].5
--mod[status:green,!status:red]|=0
--mod[status:red,status:red]|*-1
}}
Rules are evaluated in order, and all rules are evaluated, so you might find yourself using ! and the parts from prior rules to get an or sort of case.
Here's the code. Once you (and anyone else) has played with it a bit, I'll see about adding some other things. It probably needs a way to bound to the bar max and zero, round instead of floor, specify more complicated rules, maybe a stop rule syntax or some and/or syntax for rules.
//ApplyChange
on('ready',function(){
'use strict';
var modifiers = {
places: function(places){ return (number)=>parseFloat((parseInt(number,10)||0).toFixed(places)); },
'*': function(amount){ return (number)=>number*amount;},
'+': function(amount){ return (number)=>number+amount;},
'/': function(amount){ return (number)=>number/amount;},
'-': function(amount){ return (number)=>number/amount;},
'=': function(amount){ return (number)=>amount;},
'%': function(amount){ return (number)=>number%amount;}
},
matchers = {
'status': function(status){
return (token,amount,context)=>{
let sm=_.reduce(
token.get('statusmarkers').split(/,/),
(m,o)=>{let p=o.split(/@/); m[p[0]]=(p[1]||0); return m; },
{}
);
return _.has(sm,status);
};
},
'negate': function(fn){ return (token,amount,context)=>!fn(context,token); },
'tag': function(tag){ return (token,amount,context)=>_.contains(amount.tags,tag); }
},
buildMatcher = function(params){
return (function(matchers){
return function(token,amount,context){
return !_.find(matchers,(m)=>!m(token,amount,context));
};
}(_.chain(params)
.reduce((m,o)=>{
let p=_.rest(o.match(/^(!)?([^:]*):(.*)$/));
p[0]=!!p[0];
m.push({
negate: p[0],
type: p[1],
value: p[2]
});
return m;
}, [])
.map((o)=>{
if('negate'===o.type || !_.has(matchers,o.type)){
return _.constant(true);
}
return (o.negate ?
matchers.negate(matchers[o.type](o.value)) :
matchers[o.type](o.value) );
})
.value()));
},
buildModifier = function(op,amount,places){
return (_.isUndefined(places) ?
modifiers[op](amount) :
_.compose(modifiers.places(amount) , modifiers[op](amount)) );
};
on('chat:message',function(msg){
if('api' === msg.type && msg.content.match(/^!apply-change/) && playerIsGM(msg.playerid) ){
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();
}
let args = _.rest(msg.content
.replace(/<br\/>\n/g, ' ')
.replace(/(\{\{(.*?)\}\})/g," $2 ")
.split(/\s+--/)),
ids = _.pluck(msg.selected,'_id'),
context = {
bar: 1,
amount: [],
mods: []
};
// !apply-change --bar|1 --amount|-[[xdy]] --mod[status:red,!status:green,tag:fire]|*.5 --ids @{target|token_id}
// !apply-change --bar|1 --damage|[[xdy]] --mod[status:green]|*.5 --ids @{target|token_id}
_.map(args,function(arg){
let cmds=((arg.match(/([^\s]+[\|#]'[^']+'|[^\s]+[\|#]"[^"]+"|[^\s]+)/)||[])[0]||'').split(/[\|#]/),
mult=1,
cmdparts=cmds.shift().match(/^([^\[]*)(?:\[([^\]]*)\])?$/),
cmd=cmdparts[1],
params=((cmdparts[2]||'').length ? cmdparts[2].trim.split(/\s*,\s*/) : []);
switch(cmd){
case 'ids':
ids=_.union(_.rest(arg.split(/\s+/)),ids);
break;
case 'bar':
context.bar = (parseInt(cmds[0])||1);
break;
case 'damage':
mult=-1;
/* falls through */
case 'amount':
context.amount.push({
amount: mult*parseInt(cmds[0]||0),
tags: params
});
mult=1;
break;
case 'mod':
let parts=cmds[0].match(/^([\*\+\-\/=%](?:\[\d\])?)?([\d\.]*)/),
amount=parseFloat(parts[2]),
opParts=parts[1].match(/^(.)(?:\[(\d*)\])?$/),
op=opParts[1],
places=opParts[2]||0;
context.mods.push({
matcher: buildMatcher(params),
modifier: buildModifier(op,amount,places)
});
break;
}
});
// get tokens
if(ids.length){
_.chain(ids)
.uniq()
.map(function(t){
return getObj('graphic',t);
})
.reject(_.isUndefined)
.each(function(token) {
let bar=`bar${context.bar}_value`,
change=0;
_.map(context.amount, (amount)=>{
let value=amount.amount;
if(context.mods.length){
_.each(context.mods,(mod)=>{
if(mod.matcher(token,amount,context)){
value=mod.modifier(value);
}
});
}
change+=value;
});
token.set(bar,parseFloat(token.get(bar))+change);
});
}
}
});
});
Cheers!