// GroupCheck version 1.14 // Last Updated: 2025-02-22 // A script to roll checks for many tokens at once with one command. var API_Meta = API_Meta||{}; //eslint-disable-line no-var API_Meta.GroupCheck={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; {try{throw new Error('');}catch(e){API_Meta.GroupCheck.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}} /* global state, getObj, getAttrByName, on, log, sendChat, playerIsGM, _ */ const GroupCheck = (() => { "use strict"; const version = "1.15"; API_Meta.GroupCheck.version = version; const stateVersion = 8, dataVersion = 7, INCOMPATIBLE = Symbol("Incompatible Sheet"), themes = { light: { foreground: "#000", background: "#FFF", rowSeparator: "#DDD", boxBorder: "#888", errorBackground: "#FFBABA", }, dark: { foreground: "#FFF", background: "#000", rowSeparator: "#555", boxBorder: "#FFF", errorBackground: "#882020", }, }, getTheme = () => { return (state?.groupCheck?.options?.darkmode ?? false) ? themes.dark : themes.light }, // Roll appearance outputStyle = (() => { const makeBox = (header, subheader, freetext, content) => { const theme = getTheme(); return `
` + `

${header}

` + `
${subheader || ""}
` + `${content}
` + (freetext ? `
${freetext}
` : "") + "
"; }, makeRow = (pic, name, roll1, roll2, isLast) => { return `` + makeName(pic, name) + `${roll1}` + (roll2 ? `${roll2}` : "") + ""; }, makeName = (pic, name) => { const imgStyle = "display:inline-block;height:30px;width:30px;vertical-align:middle;margin-right:4px"; return "" + (pic ? `
` : "") + `${name}` + ""; }, makeCommandButton = (name, command, useBorder) => { const theme = getTheme(); const style = `style="font-weight:bold;color:${theme.foreground};background:${theme.background};border:${useBorder?`1px solid ${theme.foreground};padding:2px;margin:1px 0`:"none;padding:0"}"`; return `${name}`; }, makeInlineroll = (roll, hideformula) => { const boundary = results => { switch (detectCritical(results)) { case "crit": return ";border:2px solid #3FB315"; case "mixed": return ";border:2px solid #4A57ED"; case "fumble": return ";border:2px solid #B31515"; default: return ""; } }; return "
${roll.results.total || "0"}
`; }, rollToText = (roll) => { switch (roll.type) { case "R": { const c = (roll.mods && roll.mods.customCrit) || [{ comp: "==", point: roll.sides }], f = (roll.mods && roll.mods.customFumble) || [{ comp: "==", point: 1 }], styledRolls = roll.results.map(r => { const style = rollIsCrit(r.v, c[0].comp, c[0].point) ? " critsuccess" : (rollIsCrit(r.v, f[0].comp, f[0].point) ? " critfail" : ""); return `${r.v}`; }); return `(${styledRolls.join("+")})`; } case "M": return roll.expr.toString().replace(/(\+|-)/g, "$1 ").replace(/\*/g, "&" + "ast" + ";"); case "V": return roll.rolls.map(rollToText).join(" "); case "G": return `'(${roll.rolls.map(a => a.map(rollToText).join(" ")).join(" ")})`; default: return ""; } }, detectCritical = (roll) => { let s = []; if (roll.type === "V") s = (roll.rolls || []).map(detectCritical); if (roll.type === "G") s = _.flatten(roll.rolls || []).map(detectCritical); if (roll.type === "R") { const crit = (roll.mods && roll.mods.customCrit) || [{ comp: "==", point: roll.sides || 0 }]; const fumble = (roll.mods && roll.mods.customFumble) || [{ comp: "==", point: 1 }]; if (roll.results.some(r => rollIsCrit(r.v, crit[0].comp, crit[0].point))) s.push("crit"); if (roll.results.some(r => rollIsCrit(r.v, fumble[0].comp, fumble[0].point))) s.push("fumble"); } const c = s.includes("crit"), f = s.includes("fumble"), m = s.includes("mixed") || (c && f); return (m ? "mixed" : (c ? "crit" : (f ? "fumble" : (false)))); }, rollIsCrit = (value, comp, point) => { switch (comp) { case "==": return value == point; case "<=": return value <= point; case ">=": return value >= point; } }; return { makeBox, makeCommandButton, makeInlineroll, makeRow }; })(), // Data variables importData = { "5E-Shaped": { "Strength Save": { "name": "Strength Saving Throw", "formula": "[[d20 + @{strength_saving_throw_formula}]]", "special": "shaped" }, "Dexterity Save": { "name": "Dexterity Saving Throw", "formula": "[[d20 + @{dexterity_saving_throw_formula}]]", "special": "shaped" }, "Constitution Save": { "name": "Constitution Saving Throw", "formula": "[[d20 + @{constitution_saving_throw_formula}]]", "special": "shaped" }, "Intelligence Save": { "name": "Intelligence Saving Throw", "formula": "[[d20 + @{intelligence_saving_throw_formula}]]", "special": "shaped" }, "Wisdom Save": { "name": "Wisdom Saving Throw", "formula": "[[d20 + @{wisdom_saving_throw_formula}]]", "special": "shaped" }, "Charisma Save": { "name": "Charisma Saving Throw", "formula": "[[d20 + @{charisma_saving_throw_formula}]]", "special": "shaped" }, "Death Save": { "name": "Death Saving Throw", "formula": "[[d20 + @{death_saving_throw_formula}]]", "special": "shaped" }, "Strength Check": { "name": "Strength Check", "formula": "[[d20 + @{strength_check_formula}]]", "special": "shaped" }, "Dexterity Check": { "name": "Dexterity Check", "formula": "[[d20 + @{dexterity_check_formula}]]", "special": "shaped" }, "Constitution Check": { "name": "Constitution Check", "formula": "[[d20 + @{constitution_check_formula}]]", "special": "shaped" }, "Intelligence Check": { "name": "Intelligence Check", "formula": "[[d20 + @{intelligence_check_formula}]]", "special": "shaped" }, "Wisdom Check": { "name": "Wisdom Check", "formula": "[[d20 + @{wisdom_check_formula}]]", "special": "shaped" }, "Charisma Check": { "name": "Charisma Check", "formula": "[[d20 + @{charisma_check_formula}]]", "special": "shaped" }, "Acrobatics": { "name": "Dexterity (Acrobatics) Check", "formula": "[[d20 + @{repeating_skill_$0_formula}]]", "special": "shaped" }, "Animal Handling": { "name": "Wisdom (Animal Handling) Check", "formula": "[[d20 + @{repeating_skill_$1_formula}]]", "special": "shaped" }, "Arcana": { "name": "Intelligence (Arcana) Check", "formula": "[[d20 + @{repeating_skill_$2_formula}]]", "special": "shaped" }, "Athletics": { "name": "Strength (Athletics) Check", "formula": "[[d20 + @{repeating_skill_$3_formula}]]", "special": "shaped" }, "Deception": { "name": "Charisma (Deception) Check", "formula": "[[d20 + @{repeating_skill_$4_formula}]]", "special": "shaped" }, "History": { "name": "Intelligence (History) Check", "formula": "[[d20 + @{repeating_skill_$5_formula}]]", "special": "shaped" }, "Insight": { "name": "Wisdom (Insight) Check", "formula": "[[d20 + @{repeating_skill_$6_formula}]]", "special": "shaped" }, "Intimidation": { "name": "Charisma (Intimidation) Check", "formula": "[[d20 + @{repeating_skill_$7_formula}]]", "special": "shaped" }, "Investigation": { "name": "Intelligence (Investigation) Check", "formula": "[[d20 + @{repeating_skill_$8_formula}]]", "special": "shaped" }, "Medicine": { "name": "Wisdom (Medicine) Check", "formula": "[[d20 + @{repeating_skill_$9_formula}]]", "special": "shaped" }, "Nature": { "name": "Intelligence (Nature) Check", "formula": "[[d20 + @{repeating_skill_$10_formula}]]", "special": "shaped" }, "Perception": { "name": "Wisdom (Perception) Check", "formula": "[[d20 + @{repeating_skill_$11_formula}]]", "special": "shaped" }, "Performance": { "name": "Charisma (Performance) Check", "formula": "[[d20 + @{repeating_skill_$12_formula}]]", "special": "shaped" }, "Persuasion": { "name": "Charisma (Persuasion) Check", "formula": "[[d20 + @{repeating_skill_$13_formula}]]", "special": "shaped" }, "Religion": { "name": "Intelligence (Religion) Check", "formula": "[[d20 + @{repeating_skill_$14_formula}]]", "special": "shaped" }, "Sleight of Hand": { "name": "Dexterity (Sleight of Hand) Check", "formula": "[[d20 + @{repeating_skill_$15_formula}]]", "special": "shaped" }, "Stealth": { "name": "Dexterity (Stealth) Check", "formula": "[[d20 + @{repeating_skill_$16_formula}]]", "special": "shaped" }, "Survival": { "name": "Wisdom (Survival) Check", "formula": "[[d20 + @{repeating_skill_$17_formula}]]", "special": "shaped" }, "AC": { "name": "Armor Class", "formula": "[[@{AC}]]" } }, "Pathfinder-Official": { "Fortitude Save": { "name": "Fortitude Saving Throw", "formula": "[[d20 + @{fortitude} + @{saves_condition}]]" }, "Reflex Save": { "name": "Reflex Saving Throw", "formula": "[[d20 + @{reflex} + @{saves_condition}]]" }, "Will Save": { "name": "Will Saving Throw", "formula": "[[d20 + @{will} + @{saves_condition}]]" }, "Combat Maneuver": { "name": "Combat Maneuver", "formula": "[[d20 + @{cmb_mod} + @{attack_condition}]]" }, "Strength Check": { "name": "Strength Check", "formula": "[[d20 + @{strength_mod} + @{ability_check_condition}]]" }, "Dexterity Check": { "name": "Dexterity Check", "formula": "[[d20 + @{dexterity_mod} + @{ability_check_condition}]]" }, "Constitution Check": { "name": "Constitution Check", "formula": "[[d20 + @{constitution_mod} + @{ability_check_condition}]]" }, "Intelligence Check": { "name": "Intelligence Check", "formula": "[[d20 + @{intelligence_mod} + @{ability_check_condition}]]" }, "Wisdom Check": { "name": "Wisdom Check", "formula": "[[d20 + @{wisdom_mod} + @{ability_check_condition}]]" }, "Charisma Check": { "name": "Charisma Check", "formula": "[[d20 + @{charisma_mod} + @{ability_check_condition}]]" }, "Acrobatics": { "name": "Acrobatics Check", "formula": "[[d20 + @{acrobatics} + @{skill_condition}]]" }, "Appraise": { "name": "Appraise Check", "formula": "[[d20 + @{appraise} + @{skill_condition}]]" }, "Bluff": { "name": "Bluff Check", "formula": "[[d20 + @{bluff} + @{skill_condition}]]" }, "Climb": { "name": "Climb Check", "formula": "[[d20 + @{climb} + @{skill_condition}]]" }, "Diplomacy": { "name": "Diplomacy Check", "formula": "[[d20 + @{diplomacy} + @{skill_condition}]]" }, "Disable Device": { "name": "Disable Device Check", "formula": "[[d20 + @{disable_device} + @{skill_condition}]]" }, "Disguise": { "name": "Disguise Check", "formula": "[[d20 + @{disguise} + @{skill_condition}]]" }, "Escape Artist": { "name": "Escape Artist Check", "formula": "[[d20 + @{escape_artist} + @{skill_condition}]]" }, "Fly": { "name": "Fly Check", "formula": "[[d20 + @{fly} + @{skill_condition}]]" }, "Handle Animal": { "name": "Handle Animal Check", "formula": "[[d20 + @{handle_animal} + @{skill_condition}]]" }, "Heal": { "name": "Heal Check", "formula": "[[d20 + @{heal} + @{skill_condition}]]" }, "Intimidate": { "name": "Intimidate Check", "formula": "[[d20 + @{intimidate} + @{skill_condition}]]" }, "Knowledge (Arcana)": { "name": "Knowledge (Arcana) Check", "formula": "[[d20 + @{knowledge_arcana} + @{skill_condition}]]" }, "Knowledge (Dungeoneering)": { "name": "Knowledge (Dungeoneering) Check", "formula": "[[d20 + @{knowledge_dungeoneering} + @{skill_condition}]]" }, "Knowledge (Engineering)": { "name": "Knowledge (Engineering) Check", "formula": "[[d20 + @{knowledge_engineering} + @{skill_condition}]]" }, "Knowledge (Geography)": { "name": "Knowledge (Geography) Check", "formula": "[[d20 + @{knowledge_geography} + @{skill_condition}]]" }, "Knowledge (History)": { "name": "Knowledge (History) Check", "formula": "[[d20 + @{knowledge_history} + @{skill_condition}]]" }, "Knowledge (Local)": { "name": "Knowledge (Local) Check", "formula": "[[d20 + @{knowledge_local} + @{skill_condition}]]" }, "Knowledge (Nature)": { "name": "Knowledge (Nature) Check", "formula": "[[d20 + @{knowledge_nature} + @{skill_condition}]]" }, "Knowledge (Nobility)": { "name": "Knowledge (Nobility) Check", "formula": "[[d20 + @{knowledge_nobility} + @{skill_condition}]]" }, "Knowledge (Planes)": { "name": "Knowledge (Planes) Check", "formula": "[[d20 + @{knowledge_planes} + @{skill_condition}]]" }, "Knowledge (Religion)": { "name": "Knowledge (Religion) Check", "formula": "[[d20 + @{knowledge_religion} + @{skill_condition}]]" }, "Linguistics": { "name": "Linguistics Check", "formula": "[[d20 + @{linguistics} + @{skill_condition}]]" }, "Perception": { "name": "Perception Check", "formula": "[[d20 + @{perception} + @{skill_condition}]]" }, "Ride": { "name": "Ride Check", "formula": "[[d20 + @{ride} + @{skill_condition}]]" }, "Sense Motive": { "name": "Sense Motive Check", "formula": "[[d20 + @{sense_motive} + @{skill_condition}]]" }, "Sleight of Hand": { "name": "Sleight of Hand Check", "formula": "[[d20 + @{sleight_of_hand} + @{skill_condition}]]" }, "Spellcraft": { "name": "Spellcraft Check", "formula": "[[d20 + @{spellcraft} + @{skill_condition}]]" }, "Stealth": { "name": "Stealth Check", "formula": "[[d20 + @{stealth} + @{skill_condition}]]" }, "Survival": { "name": "Survival Check", "formula": "[[d20 + @{survival} + @{skill_condition}]]" }, "Swim": { "name": "Swim Check", "formula": "[[d20 + @{swim} + @{skill_condition}]]" }, "Use Magic Device": { "name": "Use Magic Device Check", "formula": "[[d20 + @{use_magic_device} + @{skill_condition}]]" }, "AC": { "name": "Armor Class", "formula": "[[@{AC}]]" } }, "Pathfinder-Community": { "Fortitude Save": { "name": "Fortitude Saving Throw", "formula": "[[d20 + @{Fort}]]" }, "Reflex Save": { "name": "Reflex Saving Throw", "formula": "[[d20 + @{Ref}]]" }, "Will Save": { "name": "Will Saving Throw", "formula": "[[d20 + @{Will}]]" }, "Strength Check": { "name": "Strength Check", "formula": "[[d20 + @{STR-mod} + @{checks-cond}]]" }, "Dexterity Check": { "name": "Dexterity Check", "formula": "[[d20 + @{DEX-mod} + @{checks-cond}]]" }, "Constitution Check": { "name": "Constitution Check", "formula": "[[d20 + @{CON-mod} + @{checks-cond}]]" }, "Intelligence Check": { "name": "Intelligence Check", "formula": "[[d20 + @{INT-mod} + @{checks-cond}]]" }, "Wisdom Check": { "name": "Wisdom Check", "formula": "[[d20 + @{WIS-mod} + @{checks-cond}]]" }, "Charisma Check": { "name": "Charisma Check", "formula": "[[d20 + @{CHA-mod} + @{checks-cond}]]" }, "Perception": { "name": "Perception Check", "formula": "[[d20 + @{Perception}]]" }, "Stealth": { "name": "Stealth Check", "formula": "[[d20 + @{Stealth}]]" }, "AC": { "name": "Armor Class", "formula": "[[@{AC}]]" } }, "5E-OGL": { "Strength Save": { "name": "Strength Saving Throw", "formula": "[[d20 + ([[d0 + @{strength_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_str_save}*@{npc}) [NPC]]]" }, "Dexterity Save": { "name": "Dexterity Saving Throw", "formula": "[[d20 + ([[d0 + @{dexterity_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_dex_save}*@{npc}) [NPC]]]" }, "Constitution Save": { "name": "Constitution Saving Throw", "formula": "[[d20 + ([[d0 + @{constitution_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_con_save}*@{npc}) [NPC]]]" }, "Intelligence Save": { "name": "Intelligence Saving Throw", "formula": "[[d20 + ([[d0 + @{intelligence_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_int_save}*@{npc}) [NPC]]]" }, "Wisdom Save": { "name": "Wisdom Saving Throw", "formula": "[[d20 + ([[d0 + @{wisdom_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_wis_save}*@{npc}) [NPC]]]" }, "Charisma Save": { "name": "Charisma Saving Throw", "formula": "[[d20 + ([[d0 + @{charisma_save_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_cha_save}*@{npc}) [NPC]]]" }, "Death Save": { "name": "Death Saving Throw", "formula": "[[d20 + @{death_save_bonus}@{globalsavingthrowbonus}]]" }, "Strength Check": { "name": "Strength Check", "formula": "[[d20 + @{strength_mod}]]" }, "Dexterity Check": { "name": "Dexterity Check", "formula": "[[d20 + @{dexterity_mod}]]" }, "Constitution Check": { "name": "Constitution Check", "formula": "[[d20 + @{constitution_mod}]]" }, "Intelligence Check": { "name": "Intelligence Check", "formula": "[[d20 + @{intelligence_mod}]]" }, "Wisdom Check": { "name": "Wisdom Check", "formula": "[[d20 + @{wisdom_mod}]]" }, "Charisma Check": { "name": "Charisma Check", "formula": "[[d20 + @{charisma_mod}]]" }, "Acrobatics": { "name": "Dexterity (Acrobatics) Check", "formula": "[[d20 + ([[d0 + @{acrobatics_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_acrobatics}*@{npc}) [NPC]]]" }, "Animal Handling": { "name": "Wisdom (Animal Handling) Check", "formula": "[[d20 + ([[d0 + @{animal_handling_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_animal_handling}*@{npc}) [NPC]]]" }, "Arcana": { "name": "Intelligence (Arcana) Check", "formula": "[[d20 + ([[d0 + @{arcana_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_arcana}*@{npc}) [NPC]]]" }, "Athletics": { "name": "Strength (Athletics) Check", "formula": "[[d20 + ([[d0 + @{athletics_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_athletics}*@{npc}) [NPC]]]" }, "Deception": { "name": "Charisma (Deception) Check", "formula": "[[d20 + ([[d0 + @{deception_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_deception}*@{npc}) [NPC]]]" }, "History": { "name": "Intelligence (History) Check", "formula": "[[d20 + ([[d0 + @{history_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_history}*@{npc}) [NPC]]]" }, "Insight": { "name": "Wisdom (Insight) Check", "formula": "[[d20 + ([[d0 + @{insight_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_insight}*@{npc}) [NPC]]]" }, "Intimidation": { "name": "Charisma (Intimidation) Check", "formula": "[[d20 + ([[d0 + @{intimidation_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_intimidation}*@{npc}) [NPC]]]" }, "Investigation": { "name": "Intelligence (Investigation) Check", "formula": "[[d20 + ([[d0 + @{investigation_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_investigation}*@{npc}) [NPC]]]" }, "Medicine": { "name": "Wisdom (Medicine) Check", "formula": "[[d20 + ([[d0 + @{medicine_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_medicine}*@{npc}) [NPC]]]" }, "Nature": { "name": "Intelligence (Nature) Check", "formula": "[[d20 + ([[d0 + @{nature_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_nature}*@{npc}) [NPC]]]" }, "Perception": { "name": "Wisdom (Perception) Check", "formula": "[[d20 + ([[d0 + @{perception_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_perception}*@{npc}) [NPC]]]" }, "Performance": { "name": "Charisma (Performance) Check", "formula": "[[d20 + ([[d0 + @{performance_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_performance}*@{npc}) [NPC]]]" }, "Persuasion": { "name": "Charisma (Persuasion) Check", "formula": "[[d20 + ([[d0 + @{persuasion_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_persuasion}*@{npc}) [NPC]]]" }, "Religion": { "name": "Intelligence (Religion) Check", "formula": "[[d20 + ([[d0 + @{religion_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_religion}*@{npc}) [NPC]]]" }, "Sleight of Hand": { "name": "Dexterity (Sleight of Hand) Check", "formula": "[[d20 + ([[d0 + @{sleight_of_hand_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_sleight_of_hand}*@{npc}) [NPC]]]" }, "Stealth": { "name": "Dexterity (Stealth) Check", "formula": "[[d20 + ([[d0 + @{stealth_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_stealth}*@{npc}) [NPC]]]" }, "Survival": { "name": "Wisdom (Survival) Check", "formula": "[[d20 + ([[d0 + @{survival_bonus}@{pbd_safe}]]*(1-@{npc})) [PC] + (@{npc_survival}*@{npc}) [NPC]]]" }, "AC": { "name": "Armor Class", "formula": "[[@{AC}]]" } }, "3.5": { "Fortitude Save": { "name": "Fortitude Saving Throw", "formula": "[[d20 + @{fortitude}]]" }, "Reflex Save": { "name": "Reflex Saving Throw", "formula": "[[d20 + @{reflex}]]" }, "Will Save": { "name": "Will Saving Throw", "formula": "[[d20 + @{wisdom}]]" }, "Strength Check": { "name": "Strength Check", "formula": "[[d20 + @{str-mod}]]" }, "Dexterity Check": { "name": "Dexterity Check", "formula": "[[d20 + @{dex-mod}]]" }, "Constitution Check": { "name": "Constitution Check", "formula": "[[d20 + @{con-mod}]]" }, "Intelligence Check": { "name": "Intelligence Check", "formula": "[[d20 + @{int-mod}]]" }, "Wisdom Check": { "name": "Wisdom Check", "formula": "[[d20 + @{wis-mod}]]" }, "Charisma Check": { "name": "Charisma Check", "formula": "[[d20 + @{cha-mod}]]" }, "Hide": { "name": "Hide Check", "formula": "[[d20 + @{hide}]]" }, "Listen": { "name": "Listen Check", "formula": "[[d20 + @{listen}]]" }, "Move Silently": { "name": "Move Silently Check", "formula": "[[d20 + @{movesilent}]]" }, "Spot": { "name": "Spot Check", "formula": "[[d20 + @{spot}]]" }, "AC": { "name": "Armor Class", "formula": "[[@{armorclass}]]" } } }, optsData = { list: { ro: { type: "string", def: "roll1", admissible: ["roll1", "roll2", "adv", "dis", "rollsetting"] }, die_adv: { type: "string", def: "2d20kh1" }, die_dis: { type: "string", def: "2d20kl1" }, fallback: { type: "string" }, globalmod: { type: "string" }, subheader: { type: "string", local: true }, custom: { type: "string", local: true }, button: { type: "string", local: true }, send: { type: "string", local: true }, multi: { type: "string", local: true }, input: { type: "string", local: true }, whisper: { type: "bool", def: false, negate: "public" }, hideformula: { type: "bool", def: false, negate: "showformula" }, usetokenname: { type: "bool", def: true, negate: "usecharname" }, showname: { type: "bool", def: true, negate: "hidename" }, showpicture: { type: "bool", def: true, negate: "hidepicture" }, process: { type: "bool", def: true, negate: "direct" }, showaverage: { type: "bool", local: true }, raw: { type: "string", local: true, }, title: { type: "string", local: true, }, ids: { type: "string", local: true, }, darkmode: { type: "bool", def: false, negate: "lightmode", } }, meta: {} }, // Setup checkInstall = () => { if (!state.groupCheck) initializeState(); else if (state.groupCheck.version < stateVersion) updateState(); if (state.groupCheck.dataVersion < dataVersion) updateCheckList(); // Build metadata for available options optsData.meta = { allopts: Object.keys(optsData.list), str: Object.keys(optsData.list).filter(k => optsData.list[k].type === "string"), glob: Object.keys(optsData.list).filter(k => !optsData.list[k].local), bool: Object.keys(optsData.list).filter(k => optsData.list[k].type === "bool"), boolNeg: Object.values(optsData.list).filter(v => (v.type === "bool")).map(v => v.negate) }; log(`-=> GroupCheck v${version} <=-`); }, initializeState = (isReset) => { state.groupCheck = { "checkList": {}, "options": Object.entries(optsData.list).reduce((m, [k, v]) => { if ("def" in v) m[k] = v.def; return m; }, {}), "version": stateVersion, "importInfo": "", "dataVersion": dataVersion }; log("-=> GroupCheck initialized with default settings!<=-"); if (!isReset) sendWelcomeMessage(); }, updateState = () => { switch (state.groupCheck.version) { case 1: case 2: initializeState(); break; case 3: state.groupCheck.options.showpicture = true; /* falls through */ case 4: case 5: state.groupCheck.dataVersion = 0; state.groupCheck.importInfo = ""; /* falls through */ case 6: state.groupCheck.options.showname = true; case 7: state.groupCheck.options.darkmode = false; } state.groupCheck.version = version; }, updateCheckList = () => { let changedData = false; switch (state.groupCheck.dataVersion) { case 1: case 2: case 3: case 4: case 5: if (state.groupCheck.importInfo === "Pathfinder") state.groupCheck.importInfo = "Pathfinder-Community"; if (["5E-Shaped", "5E-OGL"].includes(state.groupCheck.importInfo)) changedData = true; Object.values(state.groupCheck.checkList).forEach(o => { o.formula = o.formula.replace(/%(\S.*?)%/g, "@{$1}"); }); // falls through case 6: if (state.groupCheck.importInfo === "5E-OGL") changedData = true; } if (state.groupCheck.importInfo && changedData) { Object.assign(state.groupCheck.checkList, importData[state.groupCheck.importInfo]); log(`-=> GroupCheck has detected that you are using the ${state.groupCheck.importInfo}` + " data set and has updated your checks database automatically. Sorry for any inconvenience caused. <=-"); } state.groupCheck.dataVersion = dataVersion; }, // Utility functions safeReadJSON = (string) => { try { const o = JSON.parse(string); if (o && typeof o === "object") return o; } catch (e) { // nolint } return false; }, sendChatNoarchive = (playerid, string) => { const whisperPrefix = `/w "${(getObj("player", playerid) || {get: () => "GM"}).get("_displayname")}" `; sendChat("GroupCheck", whisperPrefix + string, null, { noarchive: true }); }, recoverInlinerollFormulae = (msg) => { return (msg.inlinerolls || []).reduce((m, v, k) => m.replace(`$[[${k}]]`, `[[${v.expression}]]`), msg.content); }, htmlReplace = (str, weak) => { const entities = { "<": "lt", ">": "gt", "'": "#39", "@": "#64", "{": "#123", "|": "#124", "}": "#125", "[": "#91", "\"": "quot", "]": "#93", "*": "#42", "&": "amp", }; const regExp = weak ? /['"@{|}[*&\]]/g : /[<>'"@{|}[*&\]]/g; return str.replace(regExp, c => ("&" + entities[c] + ";")); }, sendChatBox = (playerid, content, background) => { var theme = getTheme(); background = background || theme.background; const output = `
${content}
`; sendChatNoarchive(playerid, output); }, handleError = (playerid, errorMsg) => sendChatBox(playerid, `

Error

${errorMsg}

`, getTheme().errorBackground), getImportButton = (label) => { return outputStyle.makeCommandButton(label, `!group-check-config --import ?{Which set|${Object.keys(importData).join("|")}}`); }, getHelp = () => htmlReplace("

GroupCheck

This is an API script meant to run checks for several tokens at once. You can specify the type of check to run and it will roll it once for every selected token. Note that you will have to configure the script and import the right types of checks before you can use it.

Basic usage

Having configured some checks, you can call the script using the following syntax

!group-check [--options] --Check Command

Here, you can supply zero or more options (see the section on options for specifics) that modify what exactly is rolled. Check Command is the command associated to the check you want to run. If no valid Check Command is supplied, a list of valid commands (in the form of API buttons) is instead output to chat, allowing you to press them to roll the corresponding check.Check Command will then be rolled once for every selected token that represents a character, and the result will be output to chat.

Example

Suppose that we are using D&D 5E, and want to roll a Dexterity saving throw for every selected token, outputting the result to the GM only. The command would be

!group-check --whisper --Dexterity Save

Note that this only works after having imported the right data for the sheet you are using.

If you have two tokens selected, representing the characters Sarah and Mark, the script will output (with default settings)

Sarah: [[d20 + @{Sarah|dexterity_saving_throw_mod}]]

Mark: [[d20 + @{Mark|dexterity_saving_throw_mod}]]

Internally, the form of the check is proscribed by a formula; the formula in this case is of the form \"[[d20 + @{dexterity_saving_throw_mod}]]\", and the script will fill in the right attribute in place of \"@{dexterity_saving_throw_mod}\".

Configuration

The script is designed to be easily configured to your specific system's needs. You can configure the script using the !group-check-config command. !group-check-config accepts the following options:

Show options

Manipulating the check database

Manipulating default options

Options

Most of the following options can be supplied in two ways: you can either supply them on the command line, or change the defaults via !group-check-config. Most of the time, it is probably advisable to do the latter.

Targeting

By default, the script will be run for every selected token. Alternatively, if the --ids IDs option is specified, it will instead run for every token in IDs, which is supplied in the form of a comma-separated list of token IDs. This shouldn't normally be necessary, but it could be useful for generating GroupCheck commands via an API script.

List of options

", true), showCommandMenu = (playerid, opts) => { const optsCommand = Object.entries(opts) .map(([key, value]) => (typeof value === "boolean") ? `--${key}` : `--${key} ${value}`) .join(" "); const commandOutput = "

Available commands

" + (Object.keys(state.groupCheck.checkList) .map(s => outputStyle.makeCommandButton(s, `!group-check ${optsCommand} --${s}`, true)) .join(" ") || "It seems there are no checks defined yet. See the " + `${outputStyle.makeCommandButton("help", "!group-check --help")} for information ` + `on how to add them, or just ${getImportButton("import")} one of the built-in lists.`) + "

"; sendChatBox(playerid, commandOutput); }, getConfigTable = () => { return "

Current Options

" + "" + "" + Object.entries(state.groupCheck.options) .map(([key, value]) => ``) .join("") + "
NameValue
${key}${value}
" + "

Checks

" + "" + "" + Object.entries(state.groupCheck.checkList) .map(([key, value]) => ``) .join("") + "
CommandNameFormulaSpecial
${key}${value.name}${htmlReplace(value.formula)}${value.special||""}
"; }, sendWelcomeMessage = () => { const theme = getTheme(); const output = `/w GM
` + "It seems you are starting fresh with GroupCheck. Please refer to the " + `${outputStyle.makeCommandButton("help", "!group-check --help")} for an in-depth ` + `explanation of all the features. Would you like to ${getImportButton("import")} ` + "one of the built-in lists of checks?
"; sendChat("GroupCheck", output); }, getRollOption = (charID) => { if (charID) { switch (getAttrByName(charID, "shaped_d20")) { case "d20": return "roll1"; case "2d20kh1": case "?{Disadvantaged|No,2d20kh1|Yes,d20}": return "adv"; case "2d20kl1": case "?{Advantaged|No,2d20kl1|Yes,d20}": return "dis"; default: return "roll2"; } } else return "roll2"; }, parseOpts = (content, hasValue) => { return content.replace(/\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; }, {}); }, replaceInput = (formula, input) => { const inputs = (input || "").split(","); return formula.replace(/INPUT_(\d+)/g, (_, num) => inputs[parseInt(num)] || ""); }, processFormula = async (formula, special, charID) => { const myGetAttrByName = async (attrName) => { const result = await getSheetItem(charID, attrName); if (attrName === "npc") return result !== 0 ? "1" : "0"; if (typeof result === "number") return String(result); else return result || ""; }; // Incompatibility is just for "multi-sheet, 2014 main" and it can all be removed after the API support multi-sheets let incompatible = false; if (special === "shaped") { formula = formula.replace(/@{(.*?)}/, (_, attrName) => { const attrValue = myGetAttrByName(attrName); if (INCOMPATIBLE === attrValue){ incompatible = true; return 0; } return (attrValue.match(/{{roll1=\[\[@{(?:[a-zA-Z0-9-_])+}(?:@{d20_mod})? \+ (.*?)\]\]}}/) || ["", attrValue])[1]; }); } while (/@{(.*?)}/.test(formula)) { const qualifier = formula.match(/@{(.*?)}/)[1]; const myAttr = await myGetAttrByName(qualifier); if (INCOMPATIBLE === myAttr){ incompatible = true; break; } formula = formula.replace(/@{(.*?)}/, myAttr); } if (incompatible) { return INCOMPATIBLE; } return formula; }, //Main functions processTokenRollData = async (token, checkFormula, checkSpecial, opts) => { const charID = token.get("represents"), ro = opts.rollOption(charID), character = getObj("character", charID), displayName = opts.showname ? ((opts.usetokenname || !character) ? token.get("name") : character.get("name")) : "", tokenPic = (opts.showpicture || !displayName) ? token.get("imgsrc").replace(/(?:max|original|med)\.png/, "thumb.png") : false; let computedFormula; if (character) computedFormula = await processFormula(checkFormula, checkSpecial, charID); else if (opts.fallback) computedFormula = checkFormula.replace(/@{(.*?)}/, opts.fallback).replace(/@{(.*?)}/g, "0"); else return null; if (INCOMPATIBLE === computedFormula) { const output = (opts.whisper ? "/w GM " : "") + outputStyle.makeBox("Incompatible Sheet", opts.subheader, '', `${displayName}`); sendChat(opts.speaking, output); return null; } if (ro === "adv") computedFormula = `${computedFormula.replace(/1?d20/, opts.die_adv)} (Advantage)`; if (ro === "dis") computedFormula = `${computedFormula.replace(/1?d20/, opts.die_dis)} (Disadvantage)`; return { "pic": tokenPic, "name": displayName, "roll2": (ro === "roll2"), "formula": computedFormula, "id": token.id, }; }, sendFinalMessage = (messages, opts, checkName, rollData) => { let freetext = ""; // Format inline rolls const extractDiceRoll = roll => { if (roll.type === "V" && roll.rolls) return roll.rolls.map(extractDiceRoll).reduce((m,x) => m+x,0); if (roll.type === "G" && roll.rolls) return _.flatten(roll.rolls).map(extractDiceRoll).reduce((m,x) => m+x,0); if (roll.type === "R") return roll.results.filter(x => x.v && !x.d).map(x => x.v).reduce((m,x) => m+x,0); else return 0; }; messages.forEach((msgList, j) => { const inlinerollData = (msgList[0].inlinerolls || []).map(roll => { return { raw: extractDiceRoll(roll.results), result: roll.results.total || 0, styled: outputStyle.makeInlineroll(roll, opts.hideformula) }; }); msgList[0].content.split("
").forEach((str, n) => { rollData[j][`result_${(n+1)}`] = []; rollData[j][`raw_${(n+1)}`] = []; rollData[j][`styled_${n+1}`] = str.replace(/\$\[\[(\d+)\]\]/g, (_, number) => { rollData[j][`result_${(n+1)}`].push(inlinerollData[parseInt(number)].result); rollData[j][`raw_${(n+1)}`].push(inlinerollData[parseInt(number)].raw); return inlinerollData[parseInt(number)].styled; }); }); }); // Format rows of output const lastIndex = opts.showaverage ? rollData.length : (rollData.length - 1); const rolls = rollData.map((o, i) => { return outputStyle.makeRow(o.pic, o.name, o.styled_1, (o.roll2 ? o.styled_2 : ""), i === lastIndex); }); if (opts.showaverage) { const fakeRoll = { results: { total: (Math.round(10 * (rollData.map(o => o.result_1[0]).reduce((p, c) => p + c, 0)) / rollData.length) / 10) } }; rolls.push(outputStyle.makeRow("", "Average of rolls", outputStyle.makeInlineroll(fakeRoll, true), false, true)); } if ("button" in opts) { const commandData = opts.button.split(/\s(.+)/), commandName = commandData.shift().replace("_", " "), commandText = (commandData[0] || "").replace(/~/g, "--") .replace(/RESULTS\((.+?)\)/, rollData.map(o => o.result_1[0]).join("$1")) .replace(/IDS\((.+?)\)/, rollData.map(o => o.id).join("$1")); freetext = outputStyle.makeCommandButton(commandName, commandText); } if ("send" in opts) { const command = (opts.send || "").replace(/~/g, "--") .replace(/RESULTS\((.+?)\)/, rollData.map(o => o.result_1[0]).join("$1")) .replace(/IDS\((.+?)\)/, rollData.map(o => o.id).join("$1")); sendChat("API", command); } if ("raw" in opts) { const rawData = rollData.map((o, i) => { const styled_1 = (o.raw_1 || []).map(x => outputStyle.makeInlineroll({ results: { total: x } }, true)).join(" "); const styled_2 = (o.raw_2 || []).map(x => outputStyle.makeInlineroll({ results: { total: x } }, true)).join(" "); return outputStyle.makeRow(o.pic, o.name, styled_1, o.roll2 ? styled_2 : "", i === lastIndex); }); sendChat(opts.speaking, outputStyle.makeBox(checkName, opts.raw || "", "", rawData.join(""))); } // Combine output const output = (opts.whisper ? "/w GM " : "") + outputStyle.makeBox(checkName, opts.subheader, freetext, rolls.join("")); sendChat(opts.speaking, output); }, handleConfig = (msg) => { const opts = parseOpts(recoverInlinerollFormulae(msg), ["import", "add", "delete", "set"]), throwError = error => handleError(msg.playerid, error); let output; if (!playerIsGM(msg.playerid)) { sendChatNoarchive(msg.playerid, "Permission denied."); return; } if (opts.import) { if (opts.import in importData) { Object.assign(state.groupCheck.checkList, importData[opts.import]); state.groupCheck.importInfo = opts.import; output = `Data set ${opts.import} imported.`; } else throwError(`Data set ${opts.import} not found.`); } else if (opts.add) { const data = safeReadJSON(opts.add.replace(/\\(\[|\])/g, "$1$1").replace(/\\at/g, "@")); if (typeof data === "object") { Object.entries(data).forEach(([key, value]) => { if (!(typeof value === "object" && "name" in value && typeof value.formula === "string")) { delete data[key]; } }); Object.assign(state.groupCheck.checkList, data); output = `Checks added. The imported JSON was:
${htmlReplace(JSON.stringify(data))}`; } else throwError("Error reading input."); } else if (opts.delete) { if (opts.delete in state.groupCheck.checkList) { delete state.groupCheck.checkList[opts.delete]; output = `Check ${opts.delete} deleted.`; } else throwError(`Check called ${opts.delete} not found.`); } else if (opts.set) { const kv = opts.set.split(/\s(.+)/); if (optsData.meta.str.includes(kv[0]) && optsData.meta.glob.includes(kv[0])) state.groupCheck.options[kv[0]] = kv[1]; else if (kv[0] === "ro") { if (optsData.list.ro.admissible.includes(kv[1])) state.groupCheck.options.ro = kv[1]; else { throwError(`Roll option ${kv[1]} is invalid, sorry.`); return; } } else if (optsData.meta.bool.includes(kv[0])) state.groupCheck.options[kv[0]] = true; else if (optsData.meta.boolNeg.includes(kv[0])) { kv[0] = optsData.meta.bool[optsData.meta.boolNeg.indexOf(kv[0])]; state.groupCheck.options[kv[0]] = false; } else { throwError("Command not understood."); return; } output = `Option ${kv[0]} set to ${state.groupCheck.options[kv[0]]}.`; } else if (opts.clear) { state.groupCheck.checkList = {}; state.groupCheck.importInfo = ""; output = "All checks cleared."; } else if (opts.defaults) { state.groupCheck.options = Object.entries(optsData.list).reduce((m, [k, v]) => { if ("def" in v) m[k] = v.def; return m; }, {}); output = "All options reset to defaults."; } else if (opts.reset) { initializeState(true); output = "Everything is reset to factory settings."; } else if (opts.show) output = getConfigTable(); else output = getHelp(); if (output) sendChatBox(msg.playerid, output); return; }, handleRolls = async (msg) => { // Options processing let checkName, checkFormula, checkSpecial; let opts = parseOpts(recoverInlinerollFormulae(msg), optsData.meta.str); const checkCmd = Object.keys(state.groupCheck.checkList).find(x => x in opts), throwError = error => handleError(msg.playerid, error); // Help is useless, but on the off chance somebody will use this... if (opts.help) { sendChatBox(msg.playerid, getHelp()); return; } // Print menu if we don't know what to roll if (!checkCmd && !opts.custom) { showCommandMenu(msg.playerid, opts); return; } // Continue with options processing if (checkCmd) { checkFormula = state.groupCheck.checkList[checkCmd].formula; checkName = state.groupCheck.checkList[checkCmd].name; checkSpecial = state.groupCheck.checkList[checkCmd].special; } optsData.meta.boolNeg.forEach((name, index) => { if (name in opts) opts[optsData.meta.bool[index]] = false; }); // Handle --custom if (opts.custom) { const kv = opts.custom.replace(/\\(\[|\])/g, "$1$1").replace(/\\at/g, "@").split(/,\s?/); if (kv.length < 2) { throwError("Custom roll format invalid."); return; } checkName = kv.shift(); checkFormula = kv.join(); } // Custom title if ("title" in opts) checkName = opts.title; // Remove invalid options and check commands from opts // Plug in defaults for unspecified options opts = Object.assign({}, state.groupCheck.options, _.pick(opts, optsData.meta.allopts)); // Apply global modifier if (opts.globalmod) { if (checkFormula.search(/\]\](?=$)/) !== -1) checkFormula = checkFormula.replace(/\]\](?=$)/, ` + (${opts.globalmod}[global modifier])]]`); else checkFormula += ` + ${opts.globalmod}`; } // Replace placeholders checkFormula = replaceInput(checkFormula, opts.input); // Eliminate invalid roll option. if (!optsData.list.ro.admissible.includes(opts.ro)) { throwError(`Roll option ${opts.ro} is invalid, sorry.`); return; } // Get options into desired format opts.rollOption = (opts.ro === "rollsetting") ? getRollOption : (() => opts.ro); opts.multi = (opts.multi > 1) ? parseInt(opts.multi) : 1; opts.speaking = (msg.playerid === "API") ? "API" : `player|${msg.playerid}`; // Get list of tokens const tokenIDs = opts.ids ? opts.ids.split(",").map(x => x.trim()) : (msg.selected || []).map(obj => obj._id); // Transform tokens into nice data packages let rollData = tokenIDs.map(id => getObj("graphic", id)) .filter(x => !!x) rollData = await Promise.all(rollData.map(async (token) => { return await processTokenRollData(token, checkFormula, checkSpecial, opts); })).then((resultRollData) => { const rollData = resultRollData.reduce((m, o) => { if (o) for (let i = 0; i < opts.multi; i++) m.push(Object.assign({}, o)); return m; }, []); const sendErrorMessage = err => { const errorMessage = "Something went wrong with the roll. The command you tried was:
" + `${msg.content}
The error message generated by Roll20 is:
${err}`; throwError(errorMessage); }; if (opts.process) { Promise.all(rollData.map(o => new Promise((resolve) => { sendChat("", `${o.formula}${o.roll2 ? `
${o.formula}` : ""}`, resolve); }))) .then(messages => sendFinalMessage(messages, opts, checkName, rollData)) .catch(sendErrorMessage); } else { try { const rolls = rollData.map((roll, index, list) => { const formula = opts.hideformula ? `[[${roll.formula}]]` : roll.formula; return outputStyle.makeRow(roll.pic, roll.name, formula, (roll.roll2 ? formula : ""), index === list.length - 1); }).join(""); const output = (opts.whisper ? "/w GM " : "") + outputStyle.makeBox(checkName, opts.subheader, "", rolls); sendChat(opts.speaking, output); } catch (err) { sendErrorMessage(err); } } }); }, handleInput = (msg) => { if (msg.type === "api") { if (msg.content.search(/^!group-check($|\s)/) !== -1) handleRolls(msg); else if (msg.content.search(/^!group-check-config\b/) !== -1) handleConfig(msg); } }, registerEventHandlers = (() => on("chat:message", handleInput)); return { checkInstall, registerEventHandlers }; })(); on("ready", () => { "use strict"; GroupCheck.checkInstall(); GroupCheck.registerEventHandlers(); }); {try{throw new Error('');}catch(e){API_Meta.GroupCheck.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.GroupCheck.offset);}}