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] libTokenMarker and ApplyDamage

Hello, forum! I am working on incorporating Aaron's libTokenMarker into the ApplyDamage script for GroupCheck from Jacob. Has anyone had any luck applying the appropirate changes to ApplyDamage to get it to recognize custom Token Markers? If so, could a brother hit you up for the modified script?
IT might help if you explain more what features you are wanting to add, the Lib Token marker is for scripters to add the functionality of being able to call token makers but adding or calling to token lib markers is just one aspect. The apply damage script was written by Jacob and Lib TokenMarker by The Aaron. The original intention for apply damage was just that to apply damage to the selected group of tokens. So I suspect (I am a novice when it comes to full on scripting) depending on what you want it to do may require a bit work.
ApplyDamage can also be used to add a status marker if target fails an ability check, and that is the functionality I want to use.  Coupled with CombatMaster, it can make a one-click saving throw to add status effects on multiple tokens based on their saving throw result, then trigger another API based on what is programmed into CombatMaster for that status marker. If I can get this to work, it would consolidate three or more commands into one API trigger, and have the effects duration tracked through combat. Unfortunately, that script only recognizes legacy status markers, and I am using a Custom Token Marker set. I could use a legacy marker for the effect I want, but aesthetically, I would prefer not to have to. I have reached out to both Aaron and Jacob for further help. Aaron said to check CombatMaster for how it integrated libTokenMarkers, but that script is over 3000 lines, and I am not experienced enough to recognize all instances of TokenMarker commands within the script. I have spent about an hour trying to suss through that script, and my mind grew numb. Jacob has not responded yet... I am just hoping someone out there who knows more than I do has already put forth the effort...
1606269294
The Aaron
Roll20 Production Team
API Scripter
Give this a try.  I don't actually know how to use this script, but it seems to run and not crash...   /* global log, _, getObj, HealthColors, playerIsGM, sendChat, on, libTokenMarkers */ const ApplyDamage = (() => { const version = "1.2"; const observers = { "change": [] }; let boundedBar = false; const defaultOpts = { type: "half", ids: "", saves: "", DC: "-1", dmg: "0", bar: "1" }; const checkInstall = () => { log(`-=> ApplyDamage v${version} <=-`); if('undefined' === typeof libTokenMarkers || (['getStatus','getStatuses','getOrderedList'].find(k=> !libTokenMarkers.hasOwnProperty(k) || 'function' !== typeof libTokenMarkers[k] )) ) { // notify of the missing library sendChat('',`/w gm <div style="color:red;font-weight:bold;border:2px solid red;background-color:black;border-radius:1em;padding:1em;">Missing dependency: libTokenMarkers</div>`); } }; const getWhisperPrefix = (playerid) => { const player = getObj("player", playerid); if (player && player.get("_displayname")) { return `/w "${player.get("_displayname")}" `; } else { return "/w GM "; } }; const parseOpts = (content, hasValue) => { return content .replace(/<br\/>\n/g, " ") .replace(/({{(.*?)\s*}}\s*$)/g, "$2") .split(/\s+--/) .slice(1) .reduce((opts, arg) => { const kv = arg.split(/\s(.+)/); if (hasValue.includes(kv[0])) { opts[kv[0]] = (kv[1] || ""); } else { opts[arg] = true; } return opts; }, {}); }; const processInlinerolls = function (msg) { if (msg.inlinerolls && msg.inlinerolls.length) { return msg.inlinerolls.map(v => { const ti = v.results.rolls.filter(v2 => v2.table) .map(v2 => v2.results.map(v3 => v3.tableItem.name).join(", ")) .join(", "); return (ti.length && ti) || v.results.total || 0; }).reduce((m, v, k) => m.replace(`$[[${k}]]`, v), msg.content); } else { return msg.content; } }; const handleError = (whisper, errorMsg) => { const output = `${whisper}<div style="border:1px solid black;background:#FFBABA;padding:3px">` + `<h4>Error</h4><p>${errorMsg}</p></div>`; sendChat("ApplyDamage", output); }; const finalApply = (results, dmg, type, bar, status) => { const barCur = `bar${bar}_value`, barMax = `bar${bar}_max`; Object.entries(results).forEach(([id, saved]) => { const token = getObj("graphic", id), prev = JSON.parse(JSON.stringify(token || {})); let newValue; if (token && !saved) { if (boundedBar) { newValue = Math.min(Math.max(parseInt(token.get(barCur)) - dmg, 0), parseInt(token.get(barMax))); } else { newValue = parseInt(token.get(barCur)) - dmg; } if (status) { let tm = libTokenMarkers.getStatus(status); token.set(`status_${tm.getTag()}`, true); } } else if (token && type === "half") { if (boundedBar) { newValue = Math.min(Math.max(parseInt(token.get(barCur)) - Math.floor(dmg / 2), 0), parseInt(token.get(barMax))); } else { newValue = parseInt(token.get(barCur)) - Math.floor(dmg / 2); } } if (!_.isUndefined(newValue)) { if (Number.isNaN(newValue)) newValue = token.get(barCur); token.set(barCur, newValue); notifyObservers("change", token, prev); } }); }; const handleInput = (msg) => { if (msg.type === "api" && msg.content.search(/^!apply-damage\b/) !== -1) { const hasValue = ["ids", "saves", "DC", "type", "dmg", "bar", "status"], opts = Object.assign({}, defaultOpts, parseOpts(processInlinerolls(msg), hasValue)); opts.ids = opts.ids.split(/,\s*/g); opts.saves = opts.saves.split(/,\s*/g); opts.DC = parseInt(opts.DC); opts.dmg = parseInt(opts.dmg); if (!playerIsGM(msg.playerid) && getObj("player", msg.playerid)) { handleError(getWhisperPrefix(msg.playerid), "Permission denied."); return; } if (!["1", "2", "3"].includes(opts.bar)) { handleError(getWhisperPrefix(msg.playerid), "Invalid bar."); return; } if (opts.status === "none") { delete opts.status; } if (opts.status && 0 === libTokenMarkers.getStatuses(opts.status).length) { handleError(getWhisperPrefix(msg.playerid), "Invalid status."); return; } const results = _.reduce(opts.ids, function (m, id, k) { m[id] = parseInt(opts.saves[k] || "0") >= opts.DC; return m; }, {}); finalApply(results, opts.dmg, opts.type, opts.bar, opts.status); const output = `${ getWhisperPrefix(msg.playerid) }<div style="border:1px solid black;background:#FFF;padding:3px"><p>${ (opts.dmg ? `${opts.dmg} damage applied to tokens, with ${ (opts.type === "half" ? "half" : "no") } damage on a successful saving throw.` : "")}${ (opts.status ? ` ${opts.status} status marker applied to tokens that failed the save.` : "") }</p></div>`; sendChat("ApplyDamage", output, null, { noarchive: true }); } return; }; const notifyObservers = (event, obj, prev) => { observers[event].forEach(observer => observer(obj, prev)); }; const registerObserver = (event, observer) => { if (observer && _.isFunction(observer) && observers.hasOwnProperty(event)) { observers[event].push(observer); } else { log("ApplyDamage event registration unsuccessful."); } }; const registerEventHandlers = () => { on("chat:message", handleInput); }; on("ready", () => { checkInstall(); registerEventHandlers(); if ("undefined" !== typeof HealthColors) { ApplyDamage.registerObserver("change", HealthColors.Update); } }); return { registerObserver }; })();
1606271066

Edited 1606274322
Aaron, you rock my socks off. It works great!  Applies the right marker every time. However, Combatmaster is not triggering the Condition when the marker is added like it does when manually applied.  That was the desired end result of this endeavor. I guess I would need to provide an arg to trigger an API command. I suppose I am expecting too much out of several already brilliant scripts. EDIT If I were to want to give an option to remove a token instead of add one, what would the syntax of that be? I think I would need to add something like:  95         if (status) {  96          let tm = libTokenMarkers.getStatus(status);  97          token.set(`status_${tm.getTag()}`, false);  98        } after line 94, but not knowing JS, I can't quite figure out how to define the arg and still have the output run correctly. I played around with the args and opts, and got it to run without crashing and output the correct message, but it did not actually remove the token marker. There must be something wrong in my arg syntax. I added this const: 78      const finalApply = (results, dmg, type, bar, status, delstatus ) => { and this arg: 95 if (delstatus) { 96 let tm = libTokenMarkers.getStatus(status); 97 token.set(`status_${tm.getTag()}`, false); 98 } With these opt: 134       if (opts.delstatus === "none") { 135          delete opts.delstatus; 141         if (opts.delstatus && 0 === libTokenMarkers.getStatuses(opts.delstatus).length) { 142          handleError(getWhisperPrefix(msg.playerid), "Invalid status."); 143          return; 144        } Also, to point back up before these edits, how hard is it to apply an api command within these args? I know I am asking a lot, and I certainly do appreciate the help.
1606278112
The Aaron
Roll20 Production Team
API Scripter
Hmm... The best thing would be for CombatMaster to observe ApplyDamage and take effect when it makes a change.  That would require a change to CombatMaster though. As for issuing API commands, it can be pretty easy, but there are sometimes issues.  You'd do it like this: sendChat('','!some-command with args and args and args');
1606278170
The Aaron
Roll20 Production Team
API Scripter
The reason it's not triggering CombatMaster by default is that changes by an API script don't cause events (for the most part), so CombatMaster isn't aware of the change.
1606278472
Pat
Pro
API Scripter
...would a generic "Trigger event" script be useful? Create Object causes an event cascade - would it be useful to create a "flag" object that gets created and destroyed with key features of it signalling that it is a deliberate event flag from the API to cascade to other API events and checks? 
1606279783

Edited 1606280042
Again, many thanks. Aaron, would I place that arg in place of the libtoken arg?  So instead of  95 if (delstatus) { 96 let tm = libTokenMarkers.getStatus(status); 97 token.set(`status_${tm.getTag()}`, false); 98 } I would put  95 if (delstatus) { 96 sendChat('','!cmaster --remove,condition-concentration';} And can you suss why my original arg did not remove the tokenmarker? 11:22PM (19 minutes ago)
1606351549
Victor B.
Pro
Sheet Author
API Scripter
condition=concentration, not condition-concentration
1606351730
Victor B.
Pro
Sheet Author
API Scripter
The Aaron, making an observer on applydamage is doable.  Then I'd could call handlestatuschange which detects new icons on the token and if those icons are in CM applies the conditions.  I'll reach out to you.  Observers aren't my forte.  
Thank, Victor!  Sorry for the syntax error. I was typing that response on my phone while brushing my teeth. I noticed the error this afternoon when I went to try the sendChat command. I couldn't get it to trigger Combatmaster to add stattuses, though. I'm gonna need to learn to script so I can quit bothering you guys.
1606366287
Victor B.
Pro
Sheet Author
API Scripter
Try the CM command via macro and rule that one out.  If working, then the issue is somewhere in your script
It's not the command. It works fine via macro and direct entry in chat, copied and pasted from the script arg. I must be missing something in the arg or opt that is preventing it from firing.  I have the same problem with the remove token option as well.  The output runs correctly, but the argument is not applied in either the <delstatus> or <concen> arguments. Here is the entire altered script as I have it running now: /* global log, _, getObj, HealthColors, playerIsGM, sendChat, on, libTokenMarkers */ const ApplyDamage = (() => {   const version = "1.2";   const observers = {       "change": []     };   let boundedBar = false;   const defaultOpts = {       type: "half",       ids: "",       saves: "",       DC: "-1",       dmg: "0",       bar: "1"     };   const checkInstall = () => {       log(`-=> ApplyDamage v${version} <=-`);         if('undefined' === typeof libTokenMarkers             || (['getStatus','getStatuses','getOrderedList'].find(k=>                 !libTokenMarkers.hasOwnProperty(k) || 'function' !== typeof libTokenMarkers[k]             ))         ) {             // notify of the missing library             sendChat('',`/w gm <div style="color:red;font-weight:bold;border:2px solid red;background-color:black;border-radius:1em;padding:1em;">Missing dependency: libTokenMarkers</div>`);         }     };     const getWhisperPrefix = (playerid) => {       const player = getObj("player", playerid);       if (player && player.get("_displayname")) {         return `/w "${player.get("_displayname")}" `;       }       else {         return "/w GM ";       }     };     const parseOpts = (content, hasValue) => {       return content         .replace(/<br\/>\n/g, " ")         .replace(/({{(.*?)\s*}}\s*$)/g, "$2")         .split(/\s+--/)         .slice(1)         .reduce((opts, arg) => {           const kv = arg.split(/\s(.+)/);           if (hasValue.includes(kv[0])) {             opts[kv[0]] = (kv[1] || "");           } else {             opts[arg] = true;           }           return opts;         }, {});     };     const processInlinerolls = function (msg) {       if (msg.inlinerolls && msg.inlinerolls.length) {         return msg.inlinerolls.map(v => {           const ti = v.results.rolls.filter(v2 => v2.table)             .map(v2 => v2.results.map(v3 => v3.tableItem.name).join(", "))             .join(", ");           return (ti.length && ti) || v.results.total || 0;         }).reduce((m, v, k) => m.replace(`$[[${k}]]`, v), msg.content);       } else {         return msg.content;       }     };     const handleError = (whisper, errorMsg) => {       const output = `${whisper}<div style="border:1px solid black;background:#FFBABA;padding:3px">` +         `<h4>Error</h4><p>${errorMsg}</p></div>`;       sendChat("ApplyDamage", output);     };     const finalApply = (results, dmg, type, bar, status, delstatus, concen,) => {       const barCur = `bar${bar}_value`,         barMax = `bar${bar}_max`;       Object.entries(results).forEach(([id, saved]) => {         const token = getObj("graphic", id),           prev = JSON.parse(JSON.stringify(token || {}));         let newValue;         if (token && !saved) {           if (boundedBar) {             newValue = Math.min(Math.max(parseInt(token.get(barCur)) - dmg, 0), parseInt(token.get(barMax)));           } else {             newValue = parseInt(token.get(barCur)) - dmg;           }           if (status) {             let tm = libTokenMarkers.getStatus(status);             token.set(`status_${tm.getTag()}`, true);           }             if (delstatus) {             let tm = libTokenMarkers.getStatus(status);             token.set(`status_${tm.getTag()}`, false);           }           if (concen) {             sendChat('',"!cmaster --remove,condition=concentration");           return};         }         else if (token && type === "half") {           if (boundedBar) {             newValue = Math.min(Math.max(parseInt(token.get(barCur)) - Math.floor(dmg / 2), 0), parseInt(token.get(barMax)));           } else {             newValue = parseInt(token.get(barCur)) - Math.floor(dmg / 2);           }         }         if (!_.isUndefined(newValue)) {           if (Number.isNaN(newValue)) newValue = token.get(barCur);           token.set(barCur, newValue);           notifyObservers("change", token, prev);         }       });     };     const handleInput = (msg) => {       if (msg.type === "api" && msg.content.search(/^!apply-damage\b/) !== -1) {         const hasValue = ["ids", "saves", "DC", "type", "dmg", "bar", "status","delstatus"],           opts = Object.assign({}, defaultOpts, parseOpts(processInlinerolls(msg), hasValue));         opts.ids = opts.ids.split(/,\s*/g);         opts.saves = opts.saves.split(/,\s*/g);         opts.DC = parseInt(opts.DC);         opts.dmg = parseInt(opts.dmg);         if (!playerIsGM(msg.playerid) && getObj("player", msg.playerid)) {           handleError(getWhisperPrefix(msg.playerid), "Permission denied.");           return;         }         if (!["1", "2", "3"].includes(opts.bar)) {           handleError(getWhisperPrefix(msg.playerid), "Invalid bar.");           return;         }         if (opts.status === "none") {           delete opts.status;         }         if (opts.delstatus === "none") {           delete opts.delstatus;         }         if (opts.concen === "none") {           delete opts.concen;         }         if (opts.status && 0 === libTokenMarkers.getStatuses(opts.status).length) {           handleError(getWhisperPrefix(msg.playerid), "Invalid status.");           return;         }         if (opts.delstatus && 0 === libTokenMarkers.getStatuses(opts.delstatus).length) {           handleError(getWhisperPrefix(msg.playerid), "Invalid status.");           return;         }         const results = _.reduce(opts.ids, function (m, id, k) {           m[id] = parseInt(opts.saves[k] || "0") >= opts.DC;           return m;         }, {});         finalApply(results, opts.dmg, opts.type, opts.bar, opts.status, opts.delstatus);         const output = `${           getWhisperPrefix(msg.playerid)         }<div style="border:1px solid black;background:#FFF;padding:3px"><p>${           (opts.dmg ? `${opts.dmg} damage applied to tokens, with ${             (opts.type === "half" ? "half" : "no")           } damage on a successful saving throw.` : "")}${           (opts.status ? ` ${opts.status} status marker applied to tokens that failed the save.` : "")}${           (opts.delstatus ? ` ${opts.delstatus} status marker removed from tokens that failed the save.` : "")}${           (opts.concen ? ` Concentration status marker removed from tokens that failed the save.` : "")         }</p></div>`;         sendChat("ApplyDamage", output, null, { noarchive: true });       }       return;     };     const notifyObservers = (event, obj, prev) => {       observers[event].forEach(observer => observer(obj, prev));     };     const registerObserver = (event, observer) => {       if (observer && _.isFunction(observer) && observers.hasOwnProperty(event)) {         observers[event].push(observer);       } else {         log("ApplyDamage event registration unsuccessful.");       }     };     const registerEventHandlers = () => {       on("chat:message", handleInput);     };     on("ready", () => {       checkInstall();       registerEventHandlers();       if ("undefined" !== typeof HealthColors) {         ApplyDamage.registerObserver("change", HealthColors.Update);       }     });   return {     registerObserver   }; })();
@theAaron For some reason this script just started crashing the sandbox. It gives me a playerIsGM error: For reference, the error message generated was:  ReferenceError: playerIsGm is not defined ReferenceError: playerIsGm is not defined at apiscript.js:41594:20 at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:154:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:154:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1661:12 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) at Id.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:489) at Zd.Ld.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:94:425) at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:111:461
1606879838
The Aaron
Roll20 Production Team
API Scripter
Somewhere you have "playerIsGm", instead of "playerIsGM"  It's case sensitive.
Thanks, Aaron. Any idea why my delstatus and concen args are not firing in the above script?
1606881772

Edited 1606882097
Derrp... I can't find any instances of playerIsGm. There are only two playerIsGM in the script, and they are both correct... EDIT I even went back to Jakob's original script, and it is still doing it. 
1606883389
The Aaron
Roll20 Production Team
API Scripter
hmm. That is weird.  Is it something you can trigger, or does it happen on startup, or at random?  I'd try disabling scripts until it doesn't happen, then look at that script for a problem.   Are you using a custom character sheet?
1606883625

Edited 1606884063
5E by Roll20 sheet. It consistently happens when I push the Apply Damage button is it GroupCheck and Apply-Damage. It did happens once at a random time. I have added two new scripts. I will try to eliminate those. (They were fluff anyway) EDIT Found it! I had added a script that sent an emote to players whenever I used a gm roll. It had it miscased.  Now about the above issue with my new arts not apying...
1606884539
The Aaron
Roll20 Production Team
API Scripter
Hmm... not really sure...