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

November 24 (4 years ago)

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?

November 24 (4 years ago)

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.

November 25 (4 years ago)

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...

November 25 (4 years ago)
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
  };
})();

November 25 (4 years ago)

Edited November 25 (4 years ago)

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.



November 25 (4 years ago)
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');
November 25 (4 years ago)
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.

November 25 (4 years ago)
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? 

November 25 (4 years ago)

Edited November 25 (4 years ago)

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)

November 26 (4 years ago)
Victor B.
Pro
Sheet Author
API Scripter

condition=concentration, not condition-concentration

November 26 (4 years ago)
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.  

November 26 (4 years ago)

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.

November 26 (4 years ago)
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

November 26 (4 years ago)

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
  };
})();
December 02 (4 years ago)

@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

December 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Somewhere you have "playerIsGm", instead of "playerIsGM"  It's case sensitive.

December 02 (4 years ago)

Thanks, Aaron.


Any idea why my delstatus and concen args are not firing in the above script?

December 02 (4 years ago)

Edited December 02 (4 years ago)

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. 


December 02 (4 years ago)
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?

December 02 (4 years ago)

Edited December 02 (4 years ago)

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...

December 02 (4 years ago)
The Aaron
Roll20 Production Team
API Scripter

Hmm... not really sure...