Familiar Arcana TTRPG Logo

Apply Damage API Script - Roll20 DnD 5e

If you are a roll20 pro user. STRONGLY recommended, you can add API's to your game allowing you to spend less time on prep.

Below is the FA-Apply script which should be added to your API. Just copy the text below and name it and hit save. This will allow you to select multiple tokens and use the macro


!group-check {{

--?{Ability Save|Strength,Strength Save|Dexterity,Dexterity Save|Constitution,Constitution Save|Intelligence,Intelligence Save|Wisdom,Wisdom Save|Charisma,Charisma Save}

--process

--subheader vs DC ?{DC}

--button ApplyDamage !apply-damage

~dmg [[?{Damage}]]

~type ?{Damage on Save|Half,half|None,none}

~DC ?{DC}

~saves RESULTS(,)

~ids IDS(,)

}}


This macro will ask you for the saving throw DC, and the damage and the save type. Then it will apply damage to all characters at once. Great for spells like Fireball that hit a lot of units at once. This does require the GroupCheck API which can be installed easily. Also if you put in negative numbers it will heal all the tokens at once which is great for spells like mass healing word.

!group-check {{

--?{Ability Save|Strength,Strength Save|Dexterity,Dexterity Save}

--process

--button ApplyDamage !apply-damage

~dmg [[?{Damage}]]

~DC 60

~saves RESULTS(,)

~ids IDS(,)

}}

This macro is good for if you need to do a quick heal or damage, but don't want to make a saving throw. Great if you forgot to enter, or need to refund some hp due to resistances. Negative numbers to heal, positive to deal damage. Hit apply damage. It compares it to a 60 DC on a Strength / Dex save so the number will always deal or heal max compared to the other one.

You can also do dice rolls for example, 1d4 + 5 instead of a number, and it will roll and apply the damage. However, this does not work for healing.

/* ===============================

  Familiar Arcana   Custom Armor

  Command format:

  !armor:15con:chain mail

  !armor:10cha:robes wisdom

  =============================== */



/* global log, _, getObj, HealthColors, playerIsGM, sendChat, on */

const ApplyDamage = (() => {

 "use strict";

 const version = "1.1",

  observers = {

   "change": [],

  },

  boundedBar = false,

  checkInstall = () => {

   log(`-=> ApplyDamage v${version} <=-`);

  },

  defaultOpts = {

   type: "half",

   ids: "",

   saves: "",

   DC: "-1",

   dmg: "0",

   bar: "1"

  },

  statusMarkers = [

   "red", "blue", "green", "brown", "purple", "pink", "yellow", "dead", "skull", "sleepy", "half-heart",

   "half-haze", "interdiction", "snail", "lightning-helix", "spanner", "chained-heart", "chemical-bolt",

   "death-zone", "drink-me", "edge-crack", "ninja-mask", "stopwatch", "fishing-net", "overdrive", "strong",

   "fist", "padlock", "three-leaves", "fluffy-wing", "pummeled", "tread", "arrowed", "aura", "back-pain",

   "black-flag", "bleeding-eye", "bolt-shield", "broken-heart", "cobweb", "broken-shield", "flying-flag",

   "radioactive", "trophy", "broken-skull", "frozen-orb", "rolling-bomb", "white-tower", "grab", "screaming",

   "grenade", "sentry-gun", "all-for-one", "angel-outfit", "archery-target"

  ],

  getWhisperPrefix = (playerid) => {

   const player = getObj("player", playerid);

   if (player && player.get("_displayname")) {

    return `/w "${player.get("_displayname")}" `;

   }

   else {

    return "/w GM ";

   }

  },

  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;

    }, {});

  },

  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;

   }

  },

  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);

  },

  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) token.set(`status_${status}`, 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);

    }

   });

  },

  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 && !statusMarkers.includes(opts.status)) {

     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;

  },

  notifyObservers = (event, obj, prev) => {

   observers[event].forEach(observer => observer(obj, prev));

  },

  registerObserver = (event, observer) => {

   if (observer && _.isFunction(observer) && observers.hasOwnProperty(event)) {

    observers[event].push(observer);

   } else {

    log("ApplyDamage event registration unsuccessful.");

   }

  },

  registerEventHandlers = () => {

   on("chat:message", handleInput);

  };


 return {

  checkInstall,

  registerEventHandlers,

  registerObserver

 };

})();


on("ready", () => {

 "use strict";

 ApplyDamage.checkInstall();

 ApplyDamage.registerEventHandlers();

 if ("undefined" !== typeof HealthColors) {

  ApplyDamage.registerObserver("change", HealthColors.Update);

 }

});