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

[Help]Apply damage to tokens from attacks

So I'm running a dnd game with a bunch of homebrew things this has been going great and its been super fun to run but combat is being slowed down by huge damage numbers my players can output by buffing each other is easily 400 hit points per turn over 5 attacks and that is every turn for 3 players so it slow us down quite a bit I would like to know if there is a way of just adding a apply damage button to the text that comes from the attack so it applies to bar 1 when you click a token our damage look like this

My only solution to this problem is to turn the holy trinity of healer, dps, tank into Healer, dps, tank, and math-nerd-who-overachieves-at-the-table.  There is no api that is going to pull that off, if there is it'd literally be the most popular script on roll20.  

But if we're on the subject of unreachable goals:

1.  A group check average that doesn't take away player agency(the players all roll their own checks and the api averages them or tells me the successes vs failures).  

2.  A way to unhide a whispered roll so the players can see that nat 1 death save in all its glory.  Snippets and pasting to discord chat just doesn't do it the justice it deserves.  

January 18 (3 years ago)
Oosh
Sheet Author
API Scripter

That's really tricky to automate, since you'd need to take into account resistances, immunities and so forth. There's also the fairly major issue that people use auto-advantage/disadvantage dice (where it always throws 2 dice). In this case the 5e sheet is rolling crit damage when it isn't required - a reactive API script wouldn't know that it isn't supposed to use that damage.

You could build a token-mod macro, something like this:

!token-mod --set bar1_value|-[[?{How much damage?|0}]]

The Query is inside an inline roll, so you can enter "29 + 44 + 15 + 6" to add up the numbers automatically.


It is possible to put some little buttons in the roll template for "apply damage/half-damage/healing to selected target" to automate this per template section, but that requires redoing the roll templates with Custom Roll Parsing, which we unfortunately can't do.

January 18 (3 years ago)
Oosh
Sheet Author
API Scripter


DM Eddie said:

2.  A way to unhide a whispered roll so the players can see that nat 1 death save in all its glory.  Snippets and pasting to discord chat just doesn't do it the justice it deserves.  

Well... late Merry Christmas, I guess!




it is supposed to be crit

that is not gonna be the problem the damages below are correct and I can account for resistances perfectly well I just want something basic that gets the number heck even something that will make the attacks into having a single damage total at the end would be great help at this point

January 18 (3 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

I think Oosh meant the instances where both rolls are not each a crit. The template lists both normal and crit damage even if only one is a potential crit, and the API would need to somehow know which values to include in the total. I suppose there could be two buttons...

I dont need for it to check if it hits I can do that myself the think is more is there a way to get the lower numbers that are the damage and put them into bar 1 still thanks sry if I write like I'm angry english is not my first languague

they ate both crits because the creature was paralyzed

normally its not like that

January 18 (3 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

I think what you want would be possible with the API and the acceptance of a few limitations (like crits and resistances), but it would take more than a trivial amount of time for someone with good coding skills.

January 19 (3 years ago)

Edited March 22 (3 years ago)
Oosh
Sheet Author
API Scripter

Major update - this kid grew up and moved out of home. New version is over here


Minor update - script no longer responds to API messages (like unWhisper). This can be changed with --ignoreAPI


Ok, so I had some procrastinating to do today.

Here's a test script - see if it does what you want.

[Test Script] - autoButtons v0.1.3

A script that sends buttons (currently GM only) to apply Crit damage, Full damage, Half damage, or Healing to the selected token. Each damage roll picked up by the script will have its own buttons, and they only work for that attack. The script attempts to grab a name from the roll template, anything it finds in {{*name=<here>}}, so you could get the template name, creature or attack depending on macro order. It should be enough to ensure the buttons are attached to the right attack though.

It should pick up damage1, damage2, crit1, crit2, upcast, upcastCrit, globalDamage, globalCrit. Buttons are provided for:

- Crit: Apply all damage

- Full: Apply all non-crit damage

- Half: Apply half of non-crit damage, rounded down

- Heal: Apply all non-crit damage as healing



IMPORTANT:

- This requires token-mod, can't be bothered rewriting token functions that Aaron's already done to perfection... but hey, who the hell is playing without tokenMod?

- Only works for the Roll20 5e sheet in its current form

- The sheet silently rolls crit damage for all kinds of stuff like normal hits, spells with a save etc. I seriously can't be bothered trying to filter the stupid rolls out of every possible template combination, so if the attack shouldn't be able do crit, *don't click the crit button* :)

- CLI is basic to non-existent. A couple of things work:

!autobut --reset    -reload the 5e presets if it's not picking up templates or whatever... shouldn't need to do this currently since you can't change the preset

!autobut --bar X    -set the token bar to target with tokenMod. 1, 2 or 3 are valid. Default is 1

!autobut --ignoreAPI [ on | 1 | true | off | 0 | false ]     -whether to ignore damage rolltemplates if they're posted by API. Default is on


There are almost certainly bugs.

Click for giffage:



The test script:

/* globals log on playerIsGM, state, sendChat */
const autoButtons = (() => { //eslint-disable-line no-unused-vars

const scriptName = 'autoButtons';

const config = {
version: {
M: 0,
m: 1,
p: 3,
get: function() {
return `${this.M}.${this.m}.${this.p}`
},
getFloat: function() {
return parseFloat(`${this.M}.${this.m}${this.p}`)
}
},
settings: {
sheet: 'dnd5e_r20',
templates: {},
buttons: [],
gmOnly: true,
hpBar: 1,
ignoreAPI: 1,
},
fetchFromState: function() {
Object.assign(this.settings, state[scriptName].settings);
},
saveToState: function() {
Object.assign(state[scriptName].settings, this.settings);
},
// Provide path relative to {config.settings}, e.g. changeSetting('sheet', ['mySheet']);
changeSetting: function(pathString, newValue, confirmMessage) {
if (typeof(pathString) !== 'string' || newValue === undefined) return;
let keyName = (pathString.match(/[^/]+$/) || [])[0],
path = /.+\/.+/.test(pathString) ? pathString.match(/(.+)\/[^/]+$/)[1] : '';
let configPath = path ? h.getObjectPath(path) : config.settings;
if (configPath && keyName) {
configPath[keyName] = newValue;
if (confirmMessage) sendChat(scriptName, `/w gm ${confirmMessage}`);
this.saveToState();
return 1;
} else {
log(`${scriptName}: bad config path ${pathString}`);
return 0;
}
},
getSetting: function(pathString) {
if (typeof(pathString) !== 'string') return null;
let configValue = h.getObjectPath(pathString);
return configValue;
},
loadPreset: function() {
if (Object.keys(preset).includes(this.settings.sheet)) {
this.settings.templates = preset[this.settings.sheet].templates;
this.settings.buttons = preset[this.settings.sheet].buttons;
h.toChat(`Loaded preset: ${config.getSetting('sheet')}`);
this.saveToState();
return 1;
} else return 0;
}
};

const preset = {
dnd5e_r20: {
templates: {
names: ['atkdmg', 'dmg', 'npcfullatk', 'npcdmg'],
damageFields: ['dmg1', 'dmg2', 'globaldamage'],
critFields: ['crit1', 'crit2', 'globaldamagecrit'],
upcastDamage: ['hldmg'],
upcastCrit: ['hldmgcrit'],
},
buttons: ['damageCrit', 'damageFull', 'damageHalf', 'healingFull'],
}
}

const styles = {
error: `color: red; font-weight: bold`,
outer: `position: relative; vertical-align: middle; font-family: pictos; display: block; background: #f4e6b6; border: 1px solid black; height: 34px; line-height: 34px; text-align: right;`,
rollName: `font-family: arial; font-size: 1.1rem; color: black; font-style:italic; float:left; position:absolute; overflow: hidden; display: block;text-align: left; max-width: 80px; line-height: 1rem; top: 3px; left: 3px;`,
buttonContainer: `display: inline-block; text-align: center; vertical-align: middle; line-height: 26px; margin: auto 5px auto 5px; height: 26px; width: 26px; border: #8c6700 1px solid; box-shadow: 0px 0px 3px #805200; border-radius: 5px; background-color: whitesmoke;`,
buttonShared: `background-color: transparent; border: none; padding: 0px; width: 100%; height: 100%; overflow: hidden; white-space: nowrap;`,
crit: `color: red; font-size: 1.5rem`,
full: `color: darkred; font-size: 2.1rem`,
half: `color: black; font-family: pictos three; font-size: 2rem; padding-top:1px;`,
healFull: `color: green; font-size: 2rem`
}

const buttons = {
damageCrit: {
label: `Crit (%)`,
style: styles.crit,
math: (d, c) => -(1 * c),
content: 'kk',
},
damageFull: {
label: `Full (%)`,
style: styles.full,
math: (d) => -(1 * d),
content: 'k',
},
damageHalf: {
label: `Half (%)`,
style: styles.half,
math: (d) => -(Math.floor(0.5 * d)),
content: 'b',
},
healingFull: {
label: `Heal (%)`,
style: styles.healFull,
math: (d) => `+${(1 * d)}`,
content: '&',
},
create: function(buttonName, damage, crit) {
let btn = this[buttonName],
bar = config.getSetting('hpBar');
if (!btn || !bar > 0) return log(`${scriptName}: error creating button ${buttonName}`);
let modifier = btn.math(damage, crit);
let label = btn.label.replace(/%/, `${Math.abs(modifier)}HP`);
return `<div style="${styles.buttonContainer}" title="${label}"><a href="!token-mod --set bar${bar}_value|${modifier}" style="${styles.buttonShared}${btn.style}">${btn.content}</a></div>`;
},
getNames: function() {
return Object.entries(this).map(e => {
if (typeof(e[1]) !== 'function') return e[0]
}).filter(v => v);
}
}

const rx = {
on: /\b(1|true|on)\b/i,
off: /\b(0|false|off)\b/i
};

const initScript = () => {
setTimeout(() => {
if (!/object/i.test(typeof(['token-mod']))) return sendChat(scriptName, `/w gm <div style="${styles.error}">tokenMod not found - this script requires tokenMod to function! Aborting init...</div>`), 500
});
if (!state[scriptName] || !state[scriptName].version) {
state[scriptName] = {
version: config.version.getFloat(),
settings: config.settings,
}
} else if (state[scriptName].version < config.version.getFloat()) {
let v = state[scriptName].version;
if (v < 0.13) {
Object.assign(state[scriptName].settings, {
ignoreAPI: 1
});
}
state[scriptName].version = config.version.getFloat();
log(`====> Updated ${scriptName} to v${config.version.get()}`);
log(state[scriptName]);
}
config.fetchFromState();
if (
(!config.getSetting('templates/names') || !config.getSetting('templates/names').length) ||
(!config.getSetting('buttons') || !config.getSetting('buttons').length)) {
config.loadPreset();
h.toChat(`Error fetching config - loaded preset defaults`);
}
on('chat:message', handleInput);
log(`- Initialised ${scriptName} - v${config.version.get()} -`);
}

const sendButtons = (damage, crit, msg) => {
let gmo = config.getSetting('gmOnly') ? true : false;
let buttonHtml = '',
activeButtons = config.getSetting(`buttons`),
name = h.findName(msg.content);
name = name || `Apply:`;
activeButtons.forEach(btn => buttonHtml += buttons.create(btn, damage, crit));
const buttonTemplate = `<div style="${styles.outer}"><div style="${styles.rollName}">${name}</div>${buttonHtml}</div>`;
h.toChat(`${buttonTemplate}`, gmo);
}

const handleDamageRoll = (msg) => {
let dmgFields = config.getSetting('templates/damageFields') || [],
critFields = config.getSetting('templates/critFields') || [];
// log(`Found fields: ${dmgFields.concat(critFields).join(', ')}`);
let dmgTotal = h.processFields(dmgFields, msg),
critTotal = h.processFields(critFields, msg);
let isSpell = h.isAttackSpell(msg.content);
if (isSpell) {
let upcastDmg = config.getSetting('templates/upcastDamage') || [],
upcastCrit = config.getSetting('templates/upcastCrit') || [];
dmgTotal += h.processFields(upcastDmg, msg);
critTotal += h.processFields(upcastCrit, msg);
}
critTotal += dmgTotal;
sendButtons(dmgTotal, critTotal, msg);
}


const handleInput = (msg) => {
if (msg.type === 'api' && playerIsGM(msg.playerid) && /^!(autobut)/i.test(msg.content)) {
// h.toChat(`handle CLI`);
let cmdLine = (msg.content.match(/^![^\s]+\s+(.+)/i) || [])[1],
params = cmdLine ? cmdLine.split(/\s*--\s*/g) : [];
params.shift();
params = params.length ? params : [''];
params.forEach(param => {
let cmd = (param.match(/^([^\s]+)/) || [])[1],
args = (param.match(/\s+(.+)/) || [])[1],
oldVal,
newVal,
changed = [];
if (!cmd) return;
switch (cmd) {
case 'reset':
if (config.getSetting('sheet')) config.loadPreset();
else h.toChat(`No preset found!`);
break;
case 'bar':
newVal = parseInt(`${args}`.replace(/\D/g, ''));
if (newVal > 0 && newVal < 4) {
if (config.changeSetting('hpBar', newVal)) changed.push(`hpBar: ${newVal}`);
}
break;
case 'setPreset':
h.toChat(`Not yet implemented: ${args}`);
newVal = args.trim();
if (Object.keys(preset).includes(newVal)) {
if (config.changeSetting('sheet', newVal)) {
config.loadPreset();
changed.push(`Preset changed: ${newVal}`);
} else log(`${scriptName}: error changing preset to "${newVal}"`);
}
break;
case 'addTemplate':
h.toChat(`Not yet implemented: ${args}`);
break;
case 'removeTemplate':
h.toChat(`Not yet implemented: ${args}`);
break;
case 'addButton':
newVal = args.trim();
if (buttons.getNames().includes(newVal)) {
oldVal = config.getSetting('buttons');
if (!oldVal.includes(newVal)) {
oldVal.push(newVal);
config.changeSetting(oldVal)
changed.push(`Added button "${newVal}" ==> [ ${oldVal.join(' | ')} ]`);
} else log(`${scriptName}: unrecognised button name`);
}
break;
case 'removeButton':
h.toChat(`Not yet implemented: ${args}`);
break;
case 'ignoreAPI':
newVal = rx.off.test(args) ? 0 : rx.on.test(args) ? 1 : null;
if (newVal !== null && config.changeSetting('ignoreAPI', newVal)) {
changed.push(`ignoreAPI: ${newVal ? 'on' : 'off'}`);
} else log(`${scriptName}: error setting ignoreAPI to ${newVal}`);
break;
case 'settings':
h.toChat(`Not yet implemented: ${args}`);
break;
default:
showHelp();
}
if (changed.length) h.toChat(`Settings changed: ${changed.join('<br>')}`);
});
} else if (msg.rolltemplate && config.getSetting('templates/names').includes(msg.rolltemplate)) {
let ignoreAPI = config.getSetting('ignoreAPI');
if (ignoreAPI && /^api$/i.test(msg.playerid)) return;
handleDamageRoll(msg);
}
}

const showHelp = () => h.toChat(`Haaaaalp!`);

const h = (() => {
const processFields = (fieldArray, msg) => {
let rolls = msg.inlinerolls;
return fieldArray.reduce((m, v) => {
// log(`Processing ${v}...`)
let rxIndex = new RegExp(`{${v}=\\$\\[\\[\\d+`, 'g');
let indexResult = msg.content.match(rxIndex);
if (indexResult) {
let index = indexResult.pop().match(/\d+$/)[0];
// log(`Found index: ${index}`);
// log(`Vanilla: ${msg.inlinerolls[index].results.total}`);
let total = rolls[index].results.total;
// log(`TOTAL: ${total}`);
if (total > 0) return m + total;
}
return m;
}, 0);
}

const isAttackSpell = (msgContent) => {
const rxSpell = /{spelllevel=(cantrip|\d+)/;
return rxSpell.test(msgContent) ? 1 : 0;
}

const findName = (msgContent) => {
const rxName = /name=([^}]+)}/i;
let name = msgContent.match(rxName);
return name ? name[1] : null;
}

const getObjectPath = (pathString, baseObject = config.settings, createPath = true) => {
let parts = pathString.split(/\/+/g);
let objRef = parts.reduce((m, v) => {
if (!m) return;
if (!m[v]) {
if (createPath) m[v] = {};
else return null;
}
return m[v];
}, baseObject)
return objRef;
}
const toChat = (msg, whisper = true) => {
let prefix = whisper ? `/w gm ` : '';
sendChat(scriptName, `${prefix}${msg}`);
}
return {
processFields,
isAttackSpell,
findName,
getObjectPath,
toChat
}
})();

on('ready', () => initScript());

})();

Do you know if it has any known conflicts?  I'm wondering if it'd conflict at all with the apply damage scriptlets out there being shared.  

January 19 (3 years ago)
Oosh
Sheet Author
API Scripter

Not sure, but it calls tokenMod directly to change the HP on the token - so it should have the same behaviour. Anything that's triggered by tokenMod will also be triggered by an auto button click.

Apart from that, it's only reading chat messages like any other script, so shouldn't conflict with anything.

January 19 (3 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

The Wizard of Oosh strikes again!

January 19 (3 years ago)
timmaugh
Pro
API Scripter

The Wizard of Oosh!

Holy Oosh you are awesome i cannot believe you did this thank you so much I'm gonna test it now

You are a genius and i love you this works perfectly and its gonna make my game much faster thank you so much 

Just throwing out a minor use case that it fails with, and I think its pretty common, anything where the second damage matters, like Toll the Dead and many people have 2 handed weapons in the second damage, or Divine smite is typically setup where it does the normal damage in damage 1, and then if the creature is fiend/undead damage 2.  I don't see an option to apply damage 2 anywhere.  

January 20 (3 years ago)

Edited January 20 (3 years ago)
Oosh
Sheet Author
API Scripter

But if I add a "Damage 2" button, what happens if damage 2 is a crit?

We need:

-Damage 1

-Damage 1 crit

-Damage 2

-Damage 2 crit

That's already 8 buttons without delving into resistances. This was the problem I pointed out before stupidly writing the script :)

It is kind of setup up to be easy to add stuff to, so I can have a look when I get a minute. I just don't know where it will end....

January 20 (3 years ago)
timmaugh
Pro
API Scripter


Oosh said:

I just don't know where it will end....

And, lo, it did never end, from the first option to the last, to the last, to the last...

(Acolytes of Oosh: Options without end.)

And under such burden did Oosh toil, until he passed into memory, into legend, and into the deeper magic of song. Ascending. Transcending. Becoming.

(Acolytes of Oosh: Options without end.)

Still does the Oosh toil, somewhere beyond our ken, spinning out the options that are the very firmaments of our existence.

(Acolytes of Oosh: Options without end.)

Let us dance.




January 20 (3 years ago)
Oosh
Sheet Author
API Scripter

timmaugh said:

(Acolytes of Oosh: Options without end.)

(Acolytes of Oosh: Options without end.)

.....

Whaaaaat? How do I get rid of these acolytes?!

I know! I just need a new script, one that procedurally generates Options, and sends them off to the other scripts. For that, I'd need some kind of "meta" script.....


Minor update above - stopped autoButtons from responding to API generated damage rolls. Some idiot wrote an "unWhisper" script and the re-posts were generating new buttons.

January 20 (3 years ago)

Edited January 27 (3 years ago)

I've been using a damage listener script from a few years ago that might have some features useful to this. The version I took can be found here. It whispers buttons to the gm, using TokenMod commands that use a target token id, and only shows a button for critical damage if it finds a nat 20 in the attack roll.

It doesn't work if a player rolls the attack and damage separately, so I modified it to the version below which sends the buttons in a roll template to the gm, and to the player who rolled the damage. This required always showing the critical damage button, though. The commands in the button include queries for halving or doubling the damage. It also shows a button for healing if a healing roll is present.

Below you'll see an example of the button output. One message is only visible to the GM and one is only visible to the player who controls the character. Clicking the button prompts for a target, then shows a drop-down query for damage adjustments: Normal, Resistant (DMG/2), Vulnerable (DMG*2), and Resist+Save (DMG/4).

extractRoll = function(msg){
return _.chain(msg.inlinerolls)
.reduce(function(m,v,k){
m['$[['+k+']]']=v.results.total || 0;
return m;
},{})
.reduce(function(m,v,k){
return m.replace(k,v);
},msg.content)
.value();
}
findRollResult = function(msg, rollname, isString = 0){
let pattern = new RegExp('{{' + rollname + '=(.+?)}}');
let result = 0;
if (isString > 0) {
msg.content.replace(pattern,(match,rollResult)=>{
result = rollResult;
});
} else {
msg.content.replace(pattern,(match,rollResult)=>{
result = parseInt(rollResult);
});
}
return result;
}

// builds a comparison function for all the crit rules
var getCritComparitor = function(roll){
let comp=[];

// handle explicit custom rules
if(_.has(roll,'mods') && _.has(roll.mods,'customCrit')){
_.each(roll.mods.customCrit,function(cc){
switch(cc.comp){
case '<=':comp.push((o)=>o<=cc.point);break;
case '==':comp.push((o)=>o==cc.point);break;
case '>=':comp.push((o)=>o>=cc.point);break;
}
});
} else {
// default "max value" rule
comp.push((o)=>o==roll.sides);
}
// return a comparison function that checks each rule on a value
return function(v){
let crit=false;
_.find(comp,(f)=>crit=crit||f(v));
return crit;
};
};

var isCrit = function(roll){
var crits = 0;
// builds a comparison function for crits in this roll type
if (roll.sides == 20) {
let comp=getCritComparitor(roll);
_.each(roll.results,(r)=>{
// check each value with the comparison function
if(comp(r.v)){
// If it was a crit, report it to chat
// (replace with what you want or return true here and false outside the if for an inspection function)
// sendChat('isCrit',`The ${r.v} is a critical!`);
crits += 1;
}

});
}
return crits;
};

on("chat:message", function(orig_msg) {
if (orig_msg.rolltemplate && orig_msg.inlinerolls) {
if(/{{dmg\d=/.test(orig_msg.content)){
let msg = _.clone(orig_msg),
damageType, damageBase, damageCrit, atk1, atk2, critTarget, charName;
damageBase = damageCrit = atk1 = atk2 = crits = 0;
damageType = charName = advantage = normal = disadvantage = always = critBtn = critDmg ='';

msg.content = extractRoll(msg);
msg.content.replace(/charname=(.+?)$/,(match,charname)=>{
charName = charname;
});
damageType = findRollResult(msg, 'dmg1type', 1);
damageBase = findRollResult(msg, 'dmg1') + findRollResult(msg, 'dmg2') + findRollResult(msg, 'hldmg') + findRollResult(msg, 'globaldamage');
damageCrit = damageBase + findRollResult(msg, 'crit1') + findRollResult(msg, 'crit2') + findRollResult(msg, 'hldmgcrit') + findRollResult(msg, 'globaldamagecrit');

advantage = findRollResult(msg, 'advantage');
normal = findRollResult(msg, 'normal');
disadvantage = findRollResult(msg, 'disadvantage');
always = findRollResult(msg, 'always');

critBtn = ' | [Deal '+damageCrit+' Critical Damage](!token-mod --ids &#64;{target|token_id} --set bar1_value|-&#91;&#91;floor('+damageCrit+'&#63;{Effect|Normal,*1|Resistant,/2|Vulnerable,*2|Resist+Save,/4}&#41;&#93;&#93;)';

_.each(msg.inlinerolls,(ir)=>_.each(ir.results.rolls,(irrr)=>{
if (irrr.sides == 20) crits +=isCrit(irrr);
}));

if (damageType == 'Healing') {
//whispers the button to the character
sendChat('DmgLstn to '+charName+'','/w '+charName+' &{template:npcaction} {{name=Healing}} {{description=[Heal '+damageBase+'](!token-mod --ids &#64;{target|1|token_id} --set bar1_value|&#91;&#91;{&#64;{target|1|bar1}+'+damageBase+'+ 0d0,&#64;{target|1|bar1|max}+0d0}kl1&#93;&#93;)}}');
sendChat('DmgLstn to GM','/w gm &{template:npcaction} {{name=Healing}} {{description=[Heal '+damageBase+'](!token-mod --ids &#64;{target|1|token_id} --set bar1_value|&#91;&#91;{&#64;{target|1|bar1}+'+damageBase+'+ 0d0,&#64;{target|1|bar1|max}+0d0}kl1&#93;&#93;)}}');
}
else {
//whispers the button to the character
sendChat('DmgLstn to '+charName+'','/w '+charName+' &{template:npcaction} {{name=Damage}} {{description=[Deal '+damageBase+' Damage](!token-mod --ids &#64;{target|token_id} --set bar1_value|-&#91;&#91;floor('+damageBase+'&#63;{Effect|Normal,&ast;1|Resistant,/2|Vulnerable,&ast;2|Resist+Save,/4}&#41;&#93;&#93;)'+critBtn+'}}');
sendChat('DmgLstn to GM','/w gm &{template:npcaction} {{name=Damage}} {{description=[Deal '+damageBase+' Damage](!token-mod --ids &#64;{target|token_id} --set bar1_value|-&#91;&#91;floor('+damageBase+'&#63;{Effect|Normal,&ast;1|Resistant,/2|Vulnerable,&ast;2|Resist+Save,/4}&#41;&#93;&#93;)'+critBtn+'}}');
}
}
}
});
January 26 (3 years ago)

Any updates or anything to add to this, I feel exactly the same as he OP. 

January 27 (3 years ago)

Great work Oosh, that is a wonderful script. I do have a small improvement given you are calling tokenmod to adjust the HP. If you add ! at the end of the math function for healingFull, it will prevent over-healing. 

healingFull: {
      label: `Heal (%)`,
      style: styles.healFull,
      math: (d) => `+${(1 * d)}!`,
      content: '&',  },


You could instead of using the above edit, add the ! as it finishes building the function to prevent both over-healing and going into the negative hit points. However I need negative hit points to trigger another 'auto kill' API I have, thus will be using the above edit to stop over-healing.

return `<div
style="${styles.buttonContainer}" 
title="${label}"><a href="!token-mod --set
bar${bar}_value|${modifier}!"
style="${styles.buttonShared}${btn.style}">${btn.content}</a></div>`;
    },

This works for me i literally have nothing to add to this it works literally perfectly for what i need it